JWT认证实现详解
1. 概述
本文档详细介绍了在Spring Boot项目中实现JWT(JSON Web Token)认证的完整实现。JWT是一种基于JSON的开放标准,用于在网络应用间安全地传输信息。本项目使用JWT实现了用户身份验证和授权管理。
2. 核心组件
2.1 JWT属性配置类 (JwtProperties.java)
package com.yixueji.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* JWT配置属性类
* 通过@ConfigurationProperties注解绑定application.yml中的配置
*/
@Component
@ConfigurationProperties(prefix = "yixueji.jwt")
@Data
public class JwtProperties {
/**
* 生成jwt令牌相关配置
*/
private String SecretKey;
private long Ttl;
private String TokenName;
}
2.2 JWT常量类 (JwtClaimsConstant.java)
package com.yixueji.constant;
/**
* JWT声明中的常量定义
*/
public class JwtClaimsConstant {
// 用户ID的键名
public static final String USER_ID = "userId";
// 用户手机号的键名
public static final String PHONE = "phone";
// 用户名的键名
public static final String USERNAME = "username";
// 姓名的键名
public static final String NAME = "name";
}
2.3 JWT工具类 (JwtUtil.java)
package com.yixueji.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
/**
* JWT工具类,用于生成和解析JWT令牌
*/
public class JwtUtil {
/**
* 生成jwt令牌
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return JWT令牌字符串
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return JWT中包含的声明
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
2.4 JWT拦截器 (JwtTokenAdminInterceptor.java)
package com.yixueji.interceptor;
import com.yixueji.constant.JwtClaimsConstant;
import com.yixueji.constant.MessageConstant;
import com.yixueji.context.BaseContext;
import com.yixueji.properties.JwtProperties;
import com.yixueji.service.UserService;
import com.yixueji.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private UserService userService;
/**
* 校验jwt令牌
*
* @param request HTTP请求
* @param response HTTP响应
* @param handler 处理器
* @return 是否通过校验
* @throws Exception 异常
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户id:{}", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
response.setContentType("text/plain;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(MessageConstant.LOGIN_TIMEOUT);
return false;
}
}
}
2.5 Web MVC配置类 (WebMvcConfiguration.java)
package com.yixueji.config;
import com.yixueji.interceptor.JwtTokenAdminInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* Web MVC配置类
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry 拦截器注册表
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/login/weixin")
.excludePathPatterns("/user/register");
}
}
2.6 用户上下文 (BaseContext.java)
package com.yixueji.context;
/**
* 基于ThreadLocal的用户上下文工具类
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置当前用户ID
* @param userId 用户ID
*/
public static void setCurrentId(Long userId) {
threadLocal.set(userId);
}
/**
* 获取当前用户ID
* @return 当前用户ID
*/
public static Long getCurrentId() {
return threadLocal.get();
}
/**
* 移除当前用户ID
*/
public static void removeCurrentId() {
threadLocal.remove();
}
}
3. 配置文件
3.1 application.yml
yixueji:
jwt:
# 设置jwt签名加密时使用的秘钥
secret-key: aaaaaaaa
# 设置jwt过期时间(毫秒)
ttl: 720000000
# 设置前端传递过来的令牌名称
token-name: token
4. 使用示例
4.1 登录生成JWT (UserController.java)
@PostMapping("/login")
@ApiOperation("用户登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("用户登录:{}", userLoginDTO);
// 调用service进行用户名和密码校验
User user = userService.login(userLoginDTO);
// 登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID, user.getUserId());
String token = JwtUtil.createJWT(
jwtProperties.getSecretKey(),
jwtProperties.getTtl(),
claims);
// 构建返回对象
UserLoginVO userLoginVO = UserLoginVO.builder()
.userId(user.getUserId())
.userName(user.getUserName())
.token(token)
.build();
return Result.success(userLoginVO);
}
5. 工作流程
5.1 登录流程
- 用户提交登录请求:
- 用户通过登录接口提交用户名和密码
- 验证用户身份:
- 系统验证用户名和密码是否匹配
- 生成JWT令牌:
- 创建claims对象,包含用户ID等信息
- 使用JwtUtil.createJWT生成JWT令牌
- 令牌包含三部分:header、payload和signature
- 返回令牌:
- 将JWT令牌返回给客户端
- 客户端存储令牌(通常在localStorage或cookie中)
5.2 请求验证流程
客户端发送请求:
- 在请求头中添加JWT令牌
- 例如:
Authorization: Bearer <token>
或自定义头部
拦截器拦截请求:
- JwtTokenAdminInterceptor拦截所有配置的路径
- 从请求头获取令牌
验证令牌:
- 使用JwtUtil.parseJWT解析令牌
- 验证签名是否有效
- 检查令牌是否过期
提取用户信息:
- 从令牌中提取用户ID
- 将用户ID存储在ThreadLocal中,方便后续使用
请求处理:
- 验证通过,请求继续处理
- 验证失败,返回401未授权状态码
6. 安全注意事项
秘钥保护:
- JWT秘钥必须妥善保管,不能泄露
- 建议在生产环境使用更复杂的秘钥
令牌过期时间:
- 设置合理的过期时间,平衡安全性和用户体验
- 当前设置为720000000毫秒(约8.3天)
令牌存储:
- 客户端应安全存储令牌
- 建议使用HttpOnly cookie或安全的localStorage存储
HTTPS传输:
- 所有JWT传输应通过HTTPS进行,防止中间人攻击
刷新令牌机制:
- 考虑实现令牌刷新机制,延长用户会话
- 可以使用刷新令牌(Refresh Token)模式
7. 依赖
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Java 8以上环境可能需要添加以下依赖 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
评论区