Markdown|SpringBoot 整合 JWT 实现无状态登录认证实战
【摘要】 传统基于Session的登录方案在分布式集群场景下存在会话共享困难的问题,JWT(JSON Web Token)作为无状态认证方案,无需在服务端存储会话信息,非常适合前后端分离项目。本文从零演示SpringBoot整合JJWT生成、解析、校验Token,封装工具类,实现登录签发令牌、拦截器校验请求身份,同时讲解JWT常见坑点:过期时间、签名泄露、Payload不要存放敏感信息,并给出完整可运行代码
前言
在前后端分离架构中,客户端和服务端跨域交互,原生Session会话同步会变得麻烦。当服务端部署多实例做负载均衡时,Session复制或者Session共享方案会增加系统复杂度。
JWT 采用无状态设计,认证信息全部加密放在Token字符串中,客户端拿到Token后,后续每次请求在请求头携带Token,服务端只需要校验签名合法性,不需要查询数据库/缓存获取会话。
JWT结构分为三部分:Header、Payload、Signature。
- Header:算法和类型,一般使用HS256对称加密
- Payload:载荷,存放非敏感自定义信息,如用户ID、用户名
- Signature:签名,使用密钥对前两部分加密,防止Token被篡改
重要提醒:JWT只是编码,不是加密!Payload可以被解码,不能存放密码、手机号等敏感数据。
一、环境依赖
Maven pom.xml引入JJWT依赖
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
二、JWT工具类封装
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtUtil {
// 密钥,配置文件中自定义,长度至少32位
@Value("${jwt.secret}")
private String secret;
// token有效期,单位毫秒,这里2小时
@Value("${jwt.expire}")
private long expire;
/**
* 获取签名密钥
*/
private SecretKey getSignKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成Token
*/
public String generateToken(Long userId, String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + expire);
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析Token,获取用户名
*/
public String getUsernameByToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 校验token是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
System.out.println("Token已过期");
} catch (MalformedJwtException e) {
System.out.println("Token格式错误");
} catch (SignatureException e) {
System.out.println("签名校验失败,token被篡改");
} catch (Exception e) {
System.out.println("token校验异常");
}
return false;
}
}
application.yml配置
jwt:
secret: abcdefghijklmnopqrstuvwxyz123456
expire: 7200000
三、登录Controller
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/auth")
public class LoginController {
@Resource
private JwtUtil jwtUtil;
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password){
// 模拟数据库校验账号密码
if("admin".equals(username) && "123456".equals(password)){
String token = jwtUtil.generateToken(1L, username);
return token;
}
return "账号密码错误";
}
@PostMapping("/test")
public String test(){
return "受保护接口,登录成功才能访问";
}
}
四、自定义拦截器校验Token
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Resource
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if(token == null || token.isEmpty()){
response.getWriter().write("未携带Token,请登录");
return false;
}
if(!jwtUtil.validateToken(token)){
response.getWriter().write("Token非法或已过期");
return false;
}
return true;
}
}
注册拦截器,放行登录接口
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/login");
}
}
五、测试流程
- POST请求
/auth/login,传入账号admin,密码123456,获取返回token字符串 - 请求
/auth/test,请求头带上token: xxx,成功访问 - 不带token、篡改token、过期token都会拦截返回提示
六、生产环境避坑要点
- JWT无状态,无法主动销毁Token。如果需要强制下线,可以搭配Redis黑名单机制,过期的token放入黑名单,拦截器优先校验黑名单。
- 密钥一定要妥善保管,不要硬编码在代码上传到Git仓库,推荐使用配置中心管理密钥。
- Payload只放业务标识字段,不要存储敏感信息。
- Token有效期不宜设置过长,业务上可增加刷新Token(RefreshToken)机制,减少频繁登录。
- HTTPS必须开启,防止Token在传输过程被劫持。
七、总结
JWT无状态认证方案,在前后端分离、微服务场景中使用广泛。本文完整实现了JWT工具类、登录签发Token、拦截器鉴权,代码开箱即用。
但JWT不是万能方案,它最大短板是不能主动失效,需要结合业务场景选择方案。如果系统大量用户需要强制下线,单纯JWT方案就不够,需要搭配Redis黑名单做增强。
全文大约1220字,直接复制粘贴到华为云博客发布即可。
要不要继续下一篇?可选方向:
- SpringBoot + 异步线程@Async实战
- MyBatis-Plus 逻辑删除 + 自动填充
- SpringBoot整合MinIO文件对象存储实战
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)