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 登录流程

  1. 用户提交登录请求
    • 用户通过登录接口提交用户名和密码
  2. 验证用户身份
    • 系统验证用户名和密码是否匹配
  3. 生成JWT令牌
    • 创建claims对象,包含用户ID等信息
    • 使用JwtUtil.createJWT生成JWT令牌
    • 令牌包含三部分:header、payload和signature
  4. 返回令牌
    • 将JWT令牌返回给客户端
    • 客户端存储令牌(通常在localStorage或cookie中)

5.2 请求验证流程

  1. 客户端发送请求

    • 在请求头中添加JWT令牌
    • 例如:Authorization: Bearer <token>或自定义头部
  2. 拦截器拦截请求

    • JwtTokenAdminInterceptor拦截所有配置的路径
    • 从请求头获取令牌
  3. 验证令牌

    • 使用JwtUtil.parseJWT解析令牌
    • 验证签名是否有效
    • 检查令牌是否过期
  4. 提取用户信息

    • 从令牌中提取用户ID
    • 将用户ID存储在ThreadLocal中,方便后续使用
  5. 请求处理

    • 验证通过,请求继续处理
    • 验证失败,返回401未授权状态码

6. 安全注意事项

  1. 秘钥保护

    • JWT秘钥必须妥善保管,不能泄露
    • 建议在生产环境使用更复杂的秘钥
  2. 令牌过期时间

    • 设置合理的过期时间,平衡安全性和用户体验
    • 当前设置为720000000毫秒(约8.3天)
  3. 令牌存储

    • 客户端应安全存储令牌
    • 建议使用HttpOnly cookie或安全的localStorage存储
  4. HTTPS传输

    • 所有JWT传输应通过HTTPS进行,防止中间人攻击
  5. 刷新令牌机制

    • 考虑实现令牌刷新机制,延长用户会话
    • 可以使用刷新令牌(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>