SpringBoot3.0集成Jwt实现token验证过程
作者:爱写代码的陌
文章介绍了如何在Spring Boot项目中实现JWT(JSON Web Token)的认证与授权,主要内容包括引入依赖、创建工具类、添加拦截器进行token校验、登录接口生成token、定义UserProvider解析用户信息、自定义异常处理以及如何更新token的Audience以避免token泄露
一、pom.xml引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>二、创建工具类JwtUtils
public class JwtUtils {
private static final HashMap<String, String> _audiences = new HashMap<>();
public static String createJWT(UserProviderDto user) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
HashMap<String, Object> claims = new HashMap<>();
claims.put(Const.JWT_CLAIMS_USER, JSON.toJSONString(user));
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setAudience(updateAudience(user.getUsername()))
.setIssuedAt(new Date())
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, Const.JWT_SECRET_KEY.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(TimeUtils.getTodayEnd());
return builder.compact();
}
/**
* Token解密
*/
public static Claims parseJWT(String token) {
try {
// 得到DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(Const.JWT_SECRET_KEY.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token)
.getBody();
}
catch (Exception ex) {
return null;
}
}
/**
* 更新当前登录账号的最新状态
* @param userNo 用户名
* @return 状态标志
*/
public static String updateAudience(String userNo) {
if (LogwingStringUtils.isNullOrEmpty(userNo))
return "";
var audience = userNo + TimeUtils.getCurrentTimeStamp();
_audiences.put(userNo, audience);
return audience;
}
/**
* 判断当前token是否最新的,不是需重新登录
* @return boolean
*/
public static boolean isNewestAudience(String userNo, String audience) {
if (LogwingStringUtils.isNullOrEmpty(userNo) || LogwingStringUtils.isNullOrEmpty(audience))
return false;
if (!_audiences.containsKey(userNo))
return false;
return _audiences.get(userNo).equals(audience);
}
/**
* 获取token
* @param request 当前http请求
* @return token
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (LogwingStringUtils.isNullOrEmpty(token)) {
return "";
}
token = token.replace("Bearer ", "");
if (LogwingStringUtils.isNullOrEmpty(token)) {
return "";
}
return token;
}
}三、添加全局拦截器,校验token是否有效
public class JwtInterceptor implements HandlerInterceptor {
//主要功能拦截请求验证token
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = JwtUtils.getToken(request);
if (LogwingStringUtils.isNullOrEmpty(token)) {
throw new InvalidUserException();
}
var claims = JwtUtils.parseJWT(token);
if (claims == null) {
throw new InvalidUserException();
}
Date createTime = claims.getIssuedAt();
if (createTime == null || createTime.compareTo(TimeUtils.getTodayStart()) < 0) {
throw new InvalidUserException();
}
String audience = claims.getAudience();
if (!JwtUtils.isNewestAudience(claims.getSubject(), audience)) {
throw new InvalidUserException();
}
return true;
}
}四、将拦截器添加到WebConfig配置中
拦截请求进行token校验
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
//拦截的路径
.addPathPatterns("/**")
//不需要的拦截请求
.excludePathPatterns(
"/api/OAuth/login",
"/api/OAuth/logout",
"/api/getDate",
"/swagger-ui/**",
"/v3/**");
}
}五、登录接口增加生成token
返回前端,账号密码校验完成之后生成token,将用户基本信息存入token
public LoginOutputDto login(LoginUserInputDto input) {
Admin loginUser = adminRepository.getLoginUserByUserName(input.getUserName());
if (loginUser == null)
return new LoginOutputDto(false, "用户名不存在,请重新输入。");
var inputPassword = RSAUtils.decryptByRSA(input.getEncryptedPassword());
var encryptPassword = MD5Utils.encryptMD5(MD5Utils.encryptMD5(inputPassword) + loginUser.getSalt());
if (!encryptPassword.equals(loginUser.getPassword()))
return new LoginOutputDto(false, "密码有误,请重新输入。");
UserProviderDto userProvider = getUserProvider(loginUser);
String token = JwtUtils.createJWT(userProvider);
LoginOutputDto output = new LoginOutputDto(true, "登录成功。");
output.setAccessToken(token);
return output;
}
private UserProviderDto getUserProvider(Admin loginUser) {
UserProviderDto userProvider = new UserProviderDto();
userProvider.setId(loginUser.getId());
userProvider.setUsername(loginUser.getUsername());
userProvider.setStatus(loginUser.getStatus());
userProvider.setType(loginUser.getType());
return userProvider;
}
}六、定义UserProvider
提供从token解析出当前用户信息的接口方法
@Service
public class UserProvider implements IUserProvider {
@Autowired
private HttpServletRequest httpServletRequest;
@Override
public UserProviderDto getCurrentUser() {
String token = JwtUtils.getToken(httpServletRequest);
if (LogwingStringUtils.isNullOrEmpty(token)) {
throw new RuntimeException(Const.UN_LOGIN_TIPS);
}
Claims claims = JwtUtils.parseJWT(token);
if (claims == null) {
throw new RuntimeException(Const.UN_LOGIN_TIPS);
}
Date createTime = claims.getIssuedAt();
if (createTime == null || createTime.compareTo(TimeUtils.getTodayStart()) < 0) {
throw new RuntimeException(Const.UN_LOGIN_TIPS);
}
String user = (String) claims.get(Const.JWT_CLAIMS_USER);
return JSON.parseObject(user, UserProviderDto.class);
}
}七、定义controller接口
登录接口
@PostMapping("/api/OAuth/login")
@ResponseBody
@Operation(summary = "登录授权接口", description = "登录授权接口")
public LoginOutputDto login(@Valid @RequestBody LoginUserInputDto input) throws NoSuchAlgorithmException {
return loginService.login(input);
}退出接口
退出时更新当前账号的Audience,用来避免退出后用旧的token仍然可以通过接口token是否有效的拦截(因为jwt具有无状态性,生成token之后改token只能等设置的过期时间到了之后才能自动销毁,否则该token在过期时间之前仍然有效),具体方法见jwtutils中的updateaudience和isnewestaudience方法
@PostMapping("/api/OAuth/logout")
@ResponseBody
@Operation(summary = "退出接口")
public ResponseOutputDto logout() {
var user = userProvider.getCurrentUser();
JwtUtils.updateAudience(user.getUsername());
return new ResponseOutputDto();
}八、自定义异常InvalidUserException
并拦截全局异常InvalidUserException,捕捉拦截器抛出的异常,返回给前端
public class InvalidUserException extends Exception {
public InvalidUserException() {
super(Const.UN_LOGIN_TIPS);
}
}@ControllerAdvice
@Slf4j
public class SystemExceptionHandler {
@ExceptionHandler(InvalidUserException.class)
@ResponseBody
public LoginOutputDto doException(InvalidUserException ex) {
return new LoginOutputDto(false, ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseOutputDto doException(Exception ex) {
if (ex.getMessage().contains("未登录")) {
return new ResponseOutputDto(false, ex.getMessage());
}
log.error(ex.getMessage(), ex);
return new ResponseOutputDto(false, "程序发生错误,请联系客服。");
}
}总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
