要通过JWT简单的令牌验证和使用JSON 格式 REST风格的API进行实现登录功能先得认识JWT的RESTful。
1.JSON Web Token (JWT)
1.JWT是什么?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。
JWT的基本概念
JWT是一个紧凑的、自包含的令牌,可以在不同服务之间安全传递信息。它由三部分组成,用点号(.)分隔:
Header.Payload.Signature
JWT的三部分结构
1. Header(头部)
包含令牌的元信息,通常指定算法和令牌类型:
java
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload(载荷)
包含要传递的数据,称为Claims(声明):
java
{
"sub": "xingchen",
"iat": 1640995200,
"exp": 1641081600,
"id": 12345,
"name": "张三"
}
3. Signature(签名)
用于验证令牌的完整性和真实性:
java
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
APP_SECRET
)
JWT的工作原理
1. 生成过程
用户登录 → 服务器验证 → 生成JWT → 返回给客户端
2. 验证过程
客户端请求 → 携带JWT → 服务器验证签名 → 提取用户信息 → 执行业务逻辑
具体生成的JWT编码就如下图左边所示的代码,右边则是它的三部分结构头部,和信息
2.为什么要使用JWT?
JWT的优势
1. 无状态性
- 服务器不需要存储会话信息
- 所有必要信息都在令牌中
- 便于分布式系统扩展
2. 自包含
- 令牌包含用户身份和权限信息
- 减少数据库查询
- 提高响应速度
3. 安全性
- 数字签名防止篡改
- 可以设置过期时间
- 支持加密算法
JWT vs 传统Session
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器 |
| 状态管理 | 无状态 | 有状态 |
| 扩展性 | 容易 | 困难 |
| 跨域支持 | 优秀 | 有限 |
| 安全性 | 依赖签名 | 依赖服务器 |
JWT的使用场景
1. 身份认证
用户登录后获得JWT,后续请求携带JWT进行身份验证。
2. 信息交换
在不同服务之间安全传递用户信息。
3. 授权
JWT中包含用户权限信息,用于访问控制。
总的来说,JWT是现代Web应用中非常重要的认证和授权技术,它简化了分布式系统中的身份管理,提高了系统的可扩展性和安全性。
2.RESTful
1.RESTful是什么?
RESTful是一种软件架构风格和设计原则,用于设计网络应用程序,特别是Web API。
RESTful的基本概念
REST(Representational State Transfer,表述性状态转移)是由Roy Fielding在2000年提出的架构风格。RESTful是指遵循REST原则的API设计。
RESTful的核心原则
1. 客户端-服务器架构
- 客户端负责用户界面和用户体验
- 服务器负责数据存储、处理和业务逻辑
- 双方通过标准化的接口通信
2. 无状态性
- 每个请求都包含处理该请求所需的全部信息
- 服务器不保存客户端的会话状态
3. 可缓存性
- 响应应该明确标识是否可缓存
- 提高系统性能和可扩展性
4. 统一接口
- 使用标准的HTTP方法
- 资源通过URI标识
- 使用标准的状态码
2.RESTful的优点是什么
1. 可扩展性
- 无状态设计便于水平扩展
- 可以轻松添加新的服务器节点
- 负载均衡更加简单
2. 灵活性和可维护性
- 前后端分离,独立开发
- 客户端和服务器可以独立演进
- 支持多种客户端类型(Web、移动端、桌面应用)
3. 标准化
- 使用成熟的HTTP协议
- 统一的接口设计
- 开发者学习成本低
4. 性能优化
- 支持缓存机制
- 可以使用CDN
- 减少不必要的数据传输
RESTful vs 其他API风格
| 特性 | RESTful | GraphQL | gRPC |
|---|---|---|---|
| 协议 | HTTP | HTTP | HTTP/2 |
| 数据格式 | JSON | JSON | Protobuf |
| 缓存 | 支持 | 有限 | 不支持 |
| 实时通信 | 不支持 | 支持 | 支持 |
| 学习曲线 | 低 | 中 | 高 |
总的来说
RESTful是一种成熟、实用的API设计风格,它:
- 简化了系统架构 - 通过无状态设计提高了可扩展性
- 提高了开发效率 - 标准化的接口减少了沟通成本
- 增强了系统灵活性 - 支持多种客户端和独立部署
- 优化了性能 - 支持缓存和CDN等优化手段
3.登录功能实现
1.后端代码
1.创建模块

2.引入相关依赖
java
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.12</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jwt的依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jaxb依赖包 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
3.往resourcess中添加Spring Boot应用的配置文件

java
spring:
datasource:
username: "你的MYSQL数据库账号"
password: "你的MYSQL数据库密码"
url: jdbc:mysql:"你的MYSQL数据库URL"
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
data:
redis:
host: "redis服务器IP地址"
port: 6379 # Redis服务端口
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.1MySQL数据库创建代码
sql
/*
Navicat Premium Dump SQL
Source Server : MySQL
Source Server Type : MySQL
Source Server Version : 80021 (8.0.21)
Source Host : localhost:3306
Source Schema : house_rental
Target Server Type : MySQL
Target Server Version : 80021 (8.0.21)
File Encoding : 65001
Date: 29/10/2025 22:56:09
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', 'admin123', 'ADMIN', '13800138000', 'admin@example.com', '2025-06-18 09:40:08', '2025-06-18 09:40:08');
INSERT INTO `user` VALUES (2, '111', '111', 'USER', '11111', '11111', '2025-06-18 10:11:46', '2025-06-18 10:11:46');
SET FOREIGN_KEY_CHECKS = 1;
4.改写启动类
5.填写bean包,mapper,service,controller等基本包



UserServiceImpl
java
package com.jiangzhong.mingxing.boot.boot07.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jiangzhong.mingxing.boot.boot07.bean.User;
import com.jiangzhong.mingxing.boot.boot07.config.JwtConfig;
import com.jiangzhong.mingxing.boot.boot07.mapper.UserMapper;
import com.jiangzhong.mingxing.boot.boot07.service.UserService;
import com.jiangzhong.mingxing.boot.boot07.util.JsonResult;
import com.jiangzhong.mingxing.boot.boot07.util.ResultTool;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import org.mockito.internal.matchers.Null;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public JsonResult login(User user) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",user.getUsername());
queryWrapper.eq("password",user.getPassword());
User one = getOne(queryWrapper);
if (one == null) {
return ResultTool.fail("账号或者密码错误", 400);
}
// 2.2 正确
// 3. 生成token令牌
String token = JwtConfig.getJwtToken(one);
// 4. 保存token令牌
stringRedisTemplate.opsForValue().set("token:" + one.getId(), token, 1, TimeUnit.DAYS);
// 5. 返回token令牌
return ResultTool.success(token);
}
@Override
public JsonResult isLogin(String token) {
// 1. 检查token是否篡改过
boolean b = JwtConfig.checkToken(token);
if (!b) {
return ResultTool.fail("用户没有登陆", 401);
}
// 2. 获取到token中保存的id信息
Claims claims = JwtConfig.parseJWT(token);
Object id = claims.get("id");
// 3. 获取到redis中存储的token
String redisToken = stringRedisTemplate.opsForValue().get("token:" + id);
// 4. 检查token是否一致
return Objects.equals(token, redisToken) ? ResultTool.success("success") : ResultTool.fail("用户没有登陆", 401);
}
}

6.加入JWT

java
import com.jiangzhong.mingxing.boot.boot07.bean.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtConfig {
public static final long EXPIRE = 1000*60*60*24;
public static final String APP_SECRET = "1234";
// @param id 当前用户ID
// @param issuer 该JWT的签发者,是否使用是可选的
// @param subject 该JWT所面向的用户,是否使用是可选的
// @param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
// @param audience 接收该JWT的一方,是否使用是可选的
//生成token字符串的方法
public static String getJwtToken(User user) {
//头部信息
//头部信息
//下面这部分是payload部分
// 设置默认标签
//设置jwt所面向的用户
//设置签证生效的时间
//设置签证失效的时间
//自定义的信息,这里存储id和姓名信息
//设置token主体部分 ,存储用户信息
//下面是第三部分
// 生成的字符串就是jwt信息,这个通常要返回出去
return Jwts.builder()
.setHeaderParam("typ", "JWT") //头部信息
.setHeaderParam("alg", "HS256") //头部信息
//下面这部分是payload部分
// 设置默认标签
.setSubject("xingchen") //设置jwt所面向的用户
.setIssuedAt(new Date()) //设置签证生效的时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置签证失效的时间
//自定义的信息,这里存储id和姓名信息
.claim("id", user.getId()) //设置token主体部分 ,存储用户信息
.claim("name", user.getUsername())
//下面是第三部分
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
}
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
return true;
}
/**
* 解析JWT
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
return claims;
}
}
7.加入util

java
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@NoArgsConstructor
@Data
public class JsonResult<T> implements Serializable {
private T data;
private Boolean success;
private String message;
private Integer code;
public JsonResult(T data) {
this.data = data;
this.success = true;
this.code = 200;
}
public JsonResult(String message, Integer code) {
this.message = message;
this.code = code;
this.success = false;
}
}
java
public class ResultTool {
public static JsonResult success(Object data) {
return new JsonResult(data);
}
public static JsonResult fail(String error, int code) {
return new JsonResult(error, code);
}
}
2.简单的前端代码实现
1.首页 index
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
我是首页
</div>
</body>
</html>
<script src="../js/vue.min.js"></script>
<script src="../js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {}
},
created() {
// 1.判断是否有token
let token = localStorage.getItem('token')
if (!token) {
// 1.1 如果没有token,则跳转到登录页
location.href = 'login.html'
return
}
// 1.2 如果有token,则继续请求
axios({
method: 'get',
url: `https://localhost:8080/auth/is_login`,
// 将请求放到请求头中进行传递
headers: {
token: token
}
}).then(resp => {
if (!resp.data.success) {
location.href = 'login.html'
return
}
})
}
})
</script>
2.登录页面 login
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.input-focus:focus {
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
.shake {
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
20%, 60% {
transform: translateX(-5px);
}
40%, 80% {
transform: translateX(5px);
}
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center gradient-bg">
<div class="w-full max-w-md px-8 py-12 bg-white rounded-2xl shadow-xl" id="app">
<div class="text-center mb-10">
<i class="fas fa-user-circle text-6xl text-indigo-500 mb-4"></i>
<h1 class="text-3xl font-bold text-gray-800">欢迎回来</h1>
<p class="text-gray-500 mt-2">{{error}}</p>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">账号</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400"></i>
</div>
<input id="email" type="text" required v-model="username"
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg input-focus transition duration-200"
placeholder="example@domain.com">
</div>
<p id="emailError" class="mt-1 text-sm text-red-600 hidden">请输入有效的邮箱地址</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input id="password" name="password" type="password" required minlength="6"
v-model="password"
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg input-focus transition duration-200"
placeholder="至少6位字符">
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<i class="fas fa-eye text-gray-400 hover:text-indigo-500"></i>
</button>
</div>
<p id="passwordError" class="mt-1 text-sm text-red-600 hidden">密码长度至少6位</p>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember" name="remember" type="checkbox"
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="remember" class="ml-2 block text-sm text-gray-700">记住我</label>
</div>
<a href="#" class="text-sm text-indigo-600 hover:text-indigo-500">忘记密码?</a>
</div>
<button type="submit" @click="login"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-200">
登录
</button>
<div class="mt-6 text-center">
<p class="text-sm text-gray-600">
还没有账号?
<a href="#" class="font-medium text-indigo-600 hover:text-indigo-500">立即注册</a>
</p>
</div>
</div>
</body>
</html>
<script src="../js/vue.min.js"></script>
<script src="../js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
username: "",
password: "",
error: ''
}
},
methods: {
login() {
let data = new URLSearchParams()
data.append("username", this.username)
data.append("password", this.password)
axios({
method: 'post',
url: 'http://localhost:8080/auth/login',
data: data
}).then(resp => {
if (resp.data.success) {
// 1.保存token
localStorage.setItem('token', resp.data.data)
// 2.跳转到首页中
location.href = 'index.html'
} else {
// 1.提示登录失败
this.error = '账号或者密码错误'
}
})
}
}
})
</script>
登录页面,登陆成功后跳转首页


