JWT + 拦截器实现无状态登录

发布于:2025-08-20 ⋅ 阅读:(19) ⋅ 点赞:(0)

✅ 引言

在现代 Web 开发中,传统的 Session 认证方式在分布式、微服务架构下面临挑战:

  • Session 存储依赖服务器内存或 Redis
  • 跨域、跨服务共享 Session 复杂
  • 增加服务器负担

JWT(JSON Web Token) 提供了一种无状态的解决方案:用户登录后,服务器返回一个 Token,后续请求携带该 Token 即可完成身份验证,无需服务器存储会话信息。

本文将结合 Spring Boot 拦截器,手把手实现一个完整的 JWT 无状态登录系统。


📌 一、JWT 是什么?

JWT 是一个开放标准(RFC 7519),用于在各方之间安全地传输信息。

一个 JWT 通常由三部分组成,用 . 分隔:

xxxxx.yyyyy.zzzzz
  • Header:令牌类型和签名算法
  • Payload:存放用户信息(如用户 ID、角色、过期时间等)
  • Signature:签名,用于验证 Token 是否被篡改

优点:自包含、可扩展、跨语言、无状态。


📌 二、技术选型

  • Spring Boot 2.7+
  • JWT 库io.jsonwebtoken:jjwt
  • 加密算法:HMAC SHA256
  • 拦截器HandlerInterceptor

📌 三、项目结构

src/
├── main/
│   ├── java/
│   │   └── com/example/jwtlogin/
│   │       ├── config/            → 配置类
│   │       ├── interceptor/       → 拦截器
│   │       ├── util/              → 工具类
│   │       ├── controller/        → 控制器
│   │       └── entity/            → 实体类

📌 四、核心代码实现

4.1 添加依赖(pom.xml)

<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>

⚠️ 注意:JJWT 0.11+ 版本拆分了模块,需引入三个依赖。


4.2 JWT 工具类

@Component
public class JwtUtil {

    // 密钥(应放在配置文件中)
    private static final String SECRET = "your-256-bit-secret-your-256-bit-secret";
    
    // 过期时间:24小时
    private static final long EXPIRATION = 1000 * 60 * 60 * 24;

    /**
     * 生成 Token
     */
    public String generateToken(String username, Long userId, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("role", role);

        return Jwts.builder()
                .setSubject(username)
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
    }

    /**
     * 解析 Token
     */
    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("Token 已过期");
        } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            throw new RuntimeException("Token 无效");
        }
    }

    /**
     * 验证 Token 是否有效
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

4.3 登录控制器

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest request) {
        // 这里应调用 UserService 验证用户名密码
        // 为简化,假设用户名密码正确
        if ("admin".equals(request.getUsername()) && "123456".equals(request.getPassword())) {
            String token = jwtUtil.generateToken(request.getUsername(), 1L, "ADMIN");
            
            Map<String, Object> result = new HashMap<>();
            result.put("token", token);
            result.put("username", request.getUsername());
            result.put("role", "ADMIN");
            
            return ResponseEntity.ok(result);
        } else {
            return ResponseEntity.status(401).body(Map.of("msg", "用户名或密码错误"));
        }
    }

    /**
     * 用户登出(前端清空 Token 即可)
     */
    @PostMapping("/logout")
    public ResponseEntity<String> logout() {
        return ResponseEntity.ok("登出成功");
    }
}

// 登录请求 DTO
class LoginRequest {
    private String username;
    private String password;
    // getter & setter
}

4.4 JWT 拦截器

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    private static final String AUTH_HEADER = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";

    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        // 1. 放行 OPTIONS 请求(预检请求)
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 2. 放行登录接口
        String requestURI = request.getRequestURI();
        if ("/auth/login".equals(requestURI) || "/auth/logout".equals(requestURI)) {
            return true;
        }

        // 3. 获取并验证 Token
        String token = request.getHeader(AUTH_HEADER);
        if (token == null || !token.startsWith(TOKEN_PREFIX)) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"msg\":\"缺少 Token\"}");
            return false;
        }

        token = token.substring(TOKEN_PREFIX.length());

        try {
            Claims claims = jwtUtil.parseToken(token);
            // 将用户信息存入 request,供后续 Controller 使用
            request.setAttribute("currentUser", claims.getSubject());
            request.setAttribute("userId", claims.get("userId"));
            request.setAttribute("role", claims.get("role"));
            return true;
        } catch (RuntimeException e) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"msg\":\"" + e.getMessage() + "\"}");
            return false;
        }
    }
}

4.5 注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")        // 保护所有 API 接口
                .excludePathPatterns("/auth/**", "/public/**"); // 放行认证和公共接口
    }
}

4.6 测试受保护的接口

@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/profile")
    public Map<String, Object> getProfile(HttpServletRequest request) {
        Map<String, Object> profile = new HashMap<>();
        profile.put("username", request.getAttribute("currentUser"));
        profile.put("userId", request.getAttribute("userId"));
        profile.put("role", request.getAttribute("role"));
        profile.put("email", "admin@example.com");
        return profile;
    }

    @GetMapping("/admin/data")
    public String adminData(HttpServletRequest request) {
        String role = (String) request.getAttribute("role");
        if ("ADMIN".equals(role)) {
            return "敏感数据:只有管理员可见";
        } else {
            return "权限不足";
        }
    }
}

📌 五、测试流程

1. 登录获取 Token

curl -X POST http://localhost:8080/auth/login \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"123456"}'

响应

{
  "token": "eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy",
  "username": "admin",
  "role": "ADMIN"
}

2. 携带 Token 访问受保护接口

curl http://localhost:8080/api/profile \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy"

响应

{
  "username": "admin",
  "userId": 1,
  "role": "ADMIN",
  "email": "admin@example.com"
}

3. 不带 Token 访问 → 401 未授权


📌 六、生产环境优化建议

优化点 说明
🔐 密钥安全 SECRET 放在 application.yml 或环境变量中,不要硬编码
🔄 Token 刷新 实现 Refresh Token 机制,避免频繁登录
🛑 Token 黑名单 使用 Redis 记录已注销的 Token,防止被盗用
🧩 自定义注解 使用 @RequireAuth(role="ADMIN") 简化权限控制
📊 日志监控 记录 Token 解析失败日志,便于排查问题

✅ 总结

步骤 说明
1. 用户登录 验证账号密码,生成 JWT
2. 客户端存储 将 Token 存入 localStorage 或 Cookie
3. 携带请求 每次请求在 Authorization 头中携带 Bearer Token
4. 拦截器验证 解析 Token,校验签名和过期时间
5. 放行或拒绝 验证通过则放行,否则返回 401

💡 无状态登录的核心服务器不保存会话状态,所有信息都封装在 Token 中,由客户端负责携带和管理。


📚 推荐


网站公告

今日签到

点亮在社区的每一天
去签到