1.什么是 JWT
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:
- Header(头部) : 描述 JWT 的元数据,定义了生成签名的算法以及
Token 的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。
- Payload(载荷) : 用来存放实际需要传递的数据,包含声明(Claims),如
sub(subject,主题)、jti(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。
- Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。
JWT 通常是这样的:xxxxx.yyyyy.zzzzz。
你可以在 jwt.io 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。
Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。

使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。
2.简单案例
2.1 创建一个spring maven项目
引入以下依赖:
注意,我是用的是3.0.1的springframework版本,不同的版本之间javax.servlet-api可能会存在冲突。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
|
2.2 项目结构

2.3 数据库设计:

2.4 spring数据库配置
1 2 3 4 5 6 7 8 9 10 11 12 13
| spring: datasource: url: jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 username: 数据库账号 password: 数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus: configuration: #这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl call-setters-on-nulls: true
|
3. 项目详细代码
3.1 users实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.kd13.jwt.entity;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@Data @TableName("users") public class Users { @TableField(value = "user_pid") private Integer userPid;
@TableField(value = "user_name") private String userName;
@TableField(value = "user_password") private String userPassword;
}
|
3.2 数据库操作
1 2 3 4 5 6 7 8 9 10 11
| package com.kd13.jwt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.kd13.jwt.entity.Users; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface UsersMapper extends BaseMapper<Users> {
}
|
3.3 业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12
| package com.kd13.jwt.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.kd13.jwt.entity.Users;
public interface UserService extends IService<Users> { Integer SelectOneUser(String userName, String userPassword);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.kd13.jwt.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.kd13.jwt.entity.Users; import com.kd13.jwt.mapper.UsersMapper; import com.kd13.jwt.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
@Service public class UserServiceImpl extends ServiceImpl<UsersMapper, Users> implements UserService {
private final UsersMapper userMapper;
@Autowired public UserServiceImpl(UsersMapper usersMapper) { this.userMapper = usersMapper; }
@Override public Integer SelectOneUser(String userName, String userPassword) { QueryWrapper<Users> queryWrapper = new QueryWrapper<>(); try { queryWrapper.eq("user_name", userName).eq("user_password", userPassword); return userMapper.selectOne(queryWrapper).getUserPid(); } catch (Exception e) { return 0; } }
}
|
3.4 JWT工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package com.kd13.jwt.util;
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.stereotype.Component;
import java.util.Calendar;
@Component public class JwtTokenUtil { private static final String TOKENKEY = "thisisacode"; private static final long EXPIRATION = 86400000;
public String generateToken(Integer userId , String userName) {
Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, (int)EXPIRATION);
String token = JWT.create() .withClaim("userId", userId) .withClaim("username", userName) .withExpiresAt(instance.getTime()) .sign(Algorithm.HMAC256(TOKENKEY)) ; System.out.println(token); return token; }
public Boolean validateToken(String token) { if (token == null || token.isEmpty()) { System.out.println("Token is null or empty"); return false; } try { if (token.split("\\.").length != 3) { System.out.println("Invalid token format"); return false; } JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKENKEY)).build(); try { DecodedJWT decodedJWT = jwtVerifier.verify(token); System.out.println("用户Id:" + decodedJWT.getClaim("userId").asInt()); System.out.println("用户名:" + decodedJWT.getClaim("username").asString()); System.out.println("过期时间:" + decodedJWT.getExpiresAt()); return true; }catch (Exception e){ if (e instanceof TokenExpiredException) { System.out.println("Token已过期"); } else if (e instanceof SignatureVerificationException) { System.out.println("签名验证失败"); } else if (e instanceof JWTVerificationException) { System.out.println("JWT验证失败: " + e.getMessage()); } else { System.out.print("未知错误: "); e.printStackTrace(); } } return false; } catch (Exception e) { return false; } } }
|
3.5 控制层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package com.kd13.jwt.controller;
import com.kd13.jwt.service.impl.UserServiceImpl; import com.kd13.jwt.util.JwtTokenUtil; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @AllArgsConstructor public class AuthController { private JwtTokenUtil jwtTokenUtil;
private UserServiceImpl userService;
@GetMapping("login") public String webLogin(String username, String password, HttpServletRequest request, HttpServletResponse response){ Integer userId = userService.SelectOneUser(username, password); if (userId > 0) { String token = jwtTokenUtil.generateToken(userId,username);
Cookie cookie = new Cookie("token", token); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setMaxAge(36000); cookie.setSecure(true);
response.addCookie(cookie);
return "登录成功!\n"+token; }else { return "账号或密码错误!"; } }
@GetMapping("index") public String onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response){ Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("token".equals(cookie.getName())) { String userToken = cookie.getValue(); if (jwtTokenUtil.validateToken(userToken)) { return "登录成功!xxx"; }else { return "账号或密码错误!"; } } } } return "cookie中找不到token"; } }
|
4. 运行项目
通过对接口进行测试:
1
| http://localhost:8080/login?username=admin&password=root
|

验证是否登入成功:
1
| http://localhost:8080/index
|

可以看到之前的token直接加入到了session中进行传递,并且重启服务器也不会失效,只有当设置的有效时长失效后才会失效,也可以再浏览器中以没登陆的方式访问,可以看到是没有携带我们需要的token值的。

5. 扩展
5.1 token登出、改密后失效
使用jwt时,一般修改密码或退出登录时,需要把正在使用的token做失效处理,防止别的客户端使用失效token访问信息。
方案一:在每次修改密码或者退出登录后,修改一下自定义的盐值。当进行下次访问时,会根据自定义盐值验证token,修改了自定义盐值,自然访问不通过。
方案二:利用数据库,存放一个修改或者登出的时间,在创建token时,标注上创建时间。如果这个创建时间小于修改或登出的时间,就表示它是修改或者登出之前的token,为过期token
5.2 token的自动续期、一定时间内无操作掉线
场景:用户登陆后,token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间。避免用户正在操作时掉线重登!
实现①:在jwt生成token时先不设置过期时间,过期时间的操作放在redis中。
①:在登陆时,把用户信息(或者token)放进redis,并设置过期时间
②:如果30分钟内用户有操作,前端带着token来访问,过滤器解析token得到用户信息,去redis中验证用户信息,验证成功则在redis中增加过期时间,验证失败,返回token错误。实现了token时间的自动更新。
③:如果30分钟内用户无操作,redis中的用户信息已过期,此时再进行操作,token解析出的用户信息在redis中验证失败,则重新登录。实现了一定时间内无操作掉线!
实现②:使用access_token、refresh_token 解决
登录获取token(包括访问令牌access_token,刷新令牌refresh_token),其中access_token设置过期时间为5分钟,refresh_token设置过期时间为30分钟。不能同时过期
前端保存access_token和refresh_token,每次请求带着access_token去访问服务器资源
服务器校验access_token有效性,通过解析access_token看是否能解析出用户信息。如果用户信息为null,说明token无效,返回401,让用户重新登录
服务器端校验access_token是否过期
如果access_token没有过期,则token正常,继续执行业务逻辑
如果access_token过期,计算 过期后到当前的时间大小 是否在refresh_token过期时间之内(是否大于30 - 5 - 5 = 20分钟,为什么不是30 - 5 = 25分钟呢?主要是想对正在请求的用户token做一个缓存,保证在最后五分钟内,新、老token都有效!防止正在进行的请求token突然失效!),
如果大于refresh_token的过期时间,则表示用户长时间无操作,token真正过期了,返回401,让用户重新登录
如果小于refresh_token的过期时间,则继续让该access_token访问业务,但返回给前端标识,提示token已过期,让前端带着refresh_token去服务器获取新的access_token,并保存在前端,后续使用新的access_token去访问!
————————————————
原文链接:https://blog.csdn.net/qq_45076180/article/details/107243172