JWT 的使用

JWT 的使用

1.什么是 JWT?

JWT (JSON Web Token) 是一种基于 Token 的认证授权机制。JWT 有很强的自解释能力,自身包含了身份验证所需要的所有信息。

JWT(JSON Web Token)由三部分组成,本质上就是一个字符串:

这三部分用 . 分割,每一个部分都是一个 JSON 对象,结构是:{}.{}.{}。JSON 对象经过编码后,表现出来的形式是:xxx.yyy.zzz。

  • Header:头部。头部由两部分组成:令牌的类型和使用的签名算法。比如:算法是"HS256",类型是"JWT"。头部的内容会通过 Base64 编码成字符串,Base64 是一种编码规则,不是加密方式,可以通过 Base64 解码得到头部原本的内容。
json 复制代码
{
	"alg": "HS256",
	"typ": "JWT"
}
  • Payload:负载。负载用来保存身份验证需要的信息,同样是通过 Base64 编码将一个 JSON 对象变成字符串。并且,因为 Base64 可以被解码,所以不建议在负载中存放隐私数据,只用来存放一般性的数据。
json 复制代码
{
    "userId": "1959649015717580800"
}
  • Signature:签名。Header 和 Payload 是通过 Base64 进行编码的,可以被解码获取里面的内容。而 Signature 是通过编码后的 Header 和 Payload 以及指定的密钥,通过 Header 中指定的签名算法进行签名得到的。\(Signature = 签名算法 ( Base64(Header), Base64(Payload), 密钥 )\)。

Signature 可以用来检查 token 是否被篡改。

已知 Signature = 签名算法 ( Base64(Header), Base64(Payload), 密钥 ),相当于 y = f(x1, x2, x3)

  • 如果前端提交过来的 Header 或者 Payload 被修改,也就是 Base64(Header) 和 Base64(Payload) 发生改变,则 y* = f(x1*, x2*, x3),后端计算得到的 Signature* != 前端提交过来的 Signature,请求不会放行。
  • 如果前端提交过来的 Signature 被修改,也就是 Signature*,则 y = f(x1, x2, x3),后端计算得到的 Signature != Signature*,请求不会放行。

2.JWT 基本使用

引入依赖:

xml 复制代码
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.6</version>
</dependency>
java 复制代码
    @Test
    void generateJWT() {
        // 密钥
        String key = "qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh";

        // 负荷
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("userId", 1959649015717580800L);

        // 根据提供的字节数组长度选择适当的算法,并返回SecretKey实例
        SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
        // 两小时内有效
        Date date = new Date(System.currentTimeMillis() + 60 * 60 * 2 * 1000);

        String token = Jwts.builder()
                .claims(claims) // 在payload中存入userId
                .signWith(secretKey) // 签名加密
                .expiration(date) // 有效时间
                .compact();

        // JWT字符串作为token
        // eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDkwMTU3MTc1ODA4MDAsImV4cCI6MTc1NjE0Nzk4Mn0.JNJVZuDGrHT99tkHBkzTS83zDORbKlWnqvq3riLK8W1PgFYZieYUnBUfZSHR0F31
        System.out.println(token);
    }
java 复制代码
    @Test
    void parseJWT() {
        String key = "qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh";

        String token = "eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDkwMTU3MTc1ODA4MDAsImV4cCI6MTc1NjE0Nzk4Mn0.JNJVZuDGrHT99tkHBkzTS83zDORbKlWnqvq3riLK8W1PgFYZieYUnBUfZSHR0F31";
        // 获取SecretKey实例
        SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));

        JwtParser parser = Jwts
                .parser()
                // 用SecretKey加密,同样用SecretKey解密,解密不通过则
                .verifyWith(secretKey)
                .build();

        // 解析token
        Claims payload = parser
                .parseSignedClaims(token)
                .getPayload();

        Long userId = (Long) payload.get("userId");
        if (userId == null)
            throw new RuntimeException();

        // 1959649015717580800
        System.out.println(userId);
    }

3.SpringBoot 集成 JWT 身份验证

下面是一个简单的 demo,演示如何在 SpringBoot 项目中使用 JWT 进行身份验证:

  • 当用户访问登录、注册等接口时,不需要验证用户身份。
  • 用户登录成功后,返回一个 JWT 作为 token,保存用户的 userId。
  • 用户访问其他接口时,需要携带一个自定义请求头 Authorization,Authorization 保存的值就是 token。
  • 请求到达后端会经过拦截器(或者过滤器),在拦截器中检查是否携带 token,以及 token 是否有效。如果检查不通过,则不放行该请求。

3.1准备工作

创建 JWT 工具类:

java 复制代码
public class JwtUtil {

    /**
     * 生成token
     * @param key 密钥
     * @param expired 过期时间
     * @param map payload保存的数据
     * @return
     */
    public static String generateToken(String key, long expired, Map<String, Object> map) {
        String token = Jwts.builder()
                .claims(map)
                .signWith(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                .expiration(new Date(System.currentTimeMillis() + expired))
                .compact();
        return token;
    }

    /**
     * 解析token
     * @param token
     * @param key
     * @return
     */
    public static Claims parseToken(String token, String key) {
        JwtParser jwtParser = Jwts
                .parser()
                .verifyWith(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                .build();
        Claims claims = jwtParser
                .parseSignedClaims(token)
                .getPayload();
        return claims;
    }
}

将 JWT 需要使用的参数放在 application.yml 中方便管理和配置,同时创建一个 properties 读取 application.yml 中的配置:

yml 复制代码
jwt:
  # 密钥
  secret-key: qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh
  # 有效时间,毫秒
  expired: 7200000
  # 请求头名称
  authorization: Authorization
java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    private String secretKey;
    private long expired;
    private String authorization;
}

编写简易接口,测试身份验证是否实现:

java 复制代码
@RestController
@RequestMapping("/api")
@Validated
public class DemoController {

    @Autowired
    private DemoService demoService;

    /**
     * 用户登录,不需要身份验证
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    public Result<String> login(@NotBlank(message = "用户名不能为空") String username, @NotBlank(message = "密码不能为空") String password) {
        String token = demoService.login(username, password);
        return Result.ok(token);
    }

    /**
     * 用户注销账号,需要进行身份验证
     *
     * @return
     */
    @DeleteMapping("/remove")
    public Result remove() {
        // 空实现,仅用来测试身份验证是否生效
        return Result.ok(null);
    }
}
java 复制代码
@Service
public class DemoService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private JwtProperties jwtProperties;

    public String login(String username, String password) {
        // 检查用户是否存在
        User user = userMapper.getByUsername(username);
        if (user == null)
            throw new RuntimeException("用户不存在");

        // 检查密码是否正确
        String originPassword = user.getPassword(); // 原密码

        password = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8)); // md5加密

        if (!originPassword.equals(password))
            throw new RuntimeException("密码不正确");

        // 生成jwt
        HashMap<String, Object> map = new HashMap<>();
        map.put("userId", user.getUserId());

        return JwtUtil.generateToken(jwtProperties.getSecretKey(), jwtProperties.getExpired(), map);
    }
}

登录成功返回的数据如下,前端获取 token 后每次发起请求都会携带 token。

json 复制代码
{
    "code": 200,
    "msg": null,
    # eyJhbGciOiJIUzM4NCJ9: Header
    # eyJ1c2VySWQiOjE5NTk2NDksImV4cCI6MTc1NjIwNjE4Nn0: Payload
    # ckfbYSF7GMT4gYJpla-4-P8dWEgoj6z6BcGTEMO29zzdpkP6dKhtTak5WExwGWMM: Signature
    # 三个部分用 . 分隔
    "data": "eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDksImV4cCI6MTc1NjIwNjE4Nn0.ckfbYSF7GMT4gYJpla-4-P8dWEgoj6z6BcGTEMO29zzdpkP6dKhtTak5WExwGWMM"
}

3.2拦截器拦截请求

创建拦截器,在请求到达后端时,检查是否携带 token 并校验 token 是否是一个正确的 JWT 字符串。

java 复制代码
@Component
public class JwtInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        try {
            // 获取token,Authorization请求头中保存了token
            String token = request.getHeader(jwtProperties.getAuthorization());
            // 解析token,获取userId
            Claims claims = JwtUtil.parseToken(token, jwtProperties.getSecretKey());
            Integer userId = (Integer) claims.get("userId");

            // 保存userId并放行
            UserContext.setUserId(userId);
            return true;
        } catch (Exception e) {
            // 401:认证失败
            response.setStatus(401);
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除ThreadLocal中的数据
        UserContext.removeUserId();
    }
}

login 请求不会被拦截,可以顺利登录,得到 token;但是 remove 请求会被拦截,可以使用 Debug 观察。

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private JwtInterceptor jwtInterceptor;

    /**
     * 向Spring MVC框架注册自定义拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                // addPathPatterns: 定义拦截的请求路径
                .addPathPatterns("/**")
                // excludePathPatterns: 定义直接放行,不拦截的请求路径
                .excludePathPatterns("/api/login");

                // .addPathPatterns("/api/user/**"): 拦截所有以 /api/user 为前缀的请求
                // .excludePathPatterns("/api/user/logout"): 在拦截 /api/user/** 请求的情况下,如果是发向 /api/user/logout 的请求不会被拦截
    }
}

3.3过滤器拦截请求

过滤器和拦截器在这里的作用都一样,都是拦截请求。但是拦截器需要注册到 Spring MVC 框架中,过滤器只需要成为一个 Bean 即可,但是需要在过滤器中设置白名单,也就是不需要检查的路径。

java 复制代码
@Component
public class JwtFilter implements Filter {

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 登录请求直接放行
        if ("/api/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        try {
            // 获取token
            String token = request.getHeader(jwtProperties.getAuthorization());
            // 解析token
            Claims claims = JwtUtil.parseToken(token, jwtProperties.getSecretKey());
            Integer userId = (Integer) claims.get("userId");

            // 保存userId并放行
            UserContext.setUserId(userId);
            chain.doFilter(request, response);

        } catch (Exception e) {
            // 401:认证失败
            response.setStatus(401);
        }
    }
}
相关推荐
搜狐技术产品小编20236 分钟前
告别“黑盒”与误判:如何用“多智能体对抗辩论”重构内容安全审核系统
后端·多agent
snow@li11 分钟前
HikariCP:高性能数据库连接池全景深入梳理
java·数据库
DantyWei31 分钟前
controller注册及调用时机
后端
edwarddamon1 小时前
Spring Cloud 配置热更新与 Bean 代理机制梳理
java·后端
码事漫谈1 小时前
我用 Seed Evolving 做了个 AI 小说写作工具
后端
Raas1001 小时前
MAIGateway,魔芋企业级AI网关的FinAPI成本竞争力设计
java·后端·网关·api网关·魔芋ai·finapi·mai gateway
evans在进步2 小时前
Spring AI 从入门到实战:用 Java 实现大模型对话与 Tool Calling
java·人工智能·spring
evans在进步2 小时前
LeetCode 189 轮转数组:三次反转原地解决,图解 Java 实现
java·算法·leetcode
DantyWei2 小时前
kube-controller-manager的leader选举流程和策略
后端