Spring Boot 整合 JWT token 实现登录鉴权

1,855 阅读3分钟

前言

JWT token 是一种经过加密的字符串,其中包含了加密的载体信息(例如登录用户的信息),经过解密后可以得到加密的载体信息。

我们可以使用 JWT token 实现用户登录鉴权,例如:

前后端分离并且不使用 session 保存用户登录状态的项目中,前端登录后,服务器将 token 字符串返回给浏览器保存,此后浏览器的请求都携带 token 字符串发送。服务端解密请求的 token 字符串,确定是有效 token 后,再进行后续业务处理。

JWT token 的结构

  • 头信息
  • 有效载荷(即我们保存的信息)
  • 签名

头信息主要包含 token 的类型(即 JWT)和使用的加密算法,例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

有效载荷可以存放用户自定的内容,例如:

{
    "id": "1111",
    "nickname": "张三"
}

签名则是将头信息的base64编码和有效载荷base64编码经过加密算法后得到的字符串,以验证在传输过程中内容没有发生变化,是一个有效的 token。

将头信息的 base64 编码和有效载荷的 base64 编码以及签名拼接起来,就得到一个 JWT token 字符串,它的格式如下:

xxxxxx.yyyyyy.zzzzzz

使用

  1. 新建一个 Spring Boot 工程,引入如下依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!--JWT token-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
  1. 新建一个工具类,负责处理 token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import java.util.Date;

public class JwtUtil {

    // 过期时间: 一天
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    // 加密密钥
    public static final String APP_SECRET = "abcdefg";

    /**
     * 生成 token 字符串
     * @param id
     * @param nickname
     * @return
     */
    public static String getJwtToken(String id, String nickname) {
        String jwtToken = Jwts.builder()
                // 设置 token 头部分
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                // 设置 token 的主题 subject,自定义
                .setSubject("token-demo")
                // 设置 token 的创建时间
                .setIssuedAt(new Date())
                // 设置过期时间,于何时过期
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                // 设置 token 的有效载荷
                .claim("id", id)
                .claim("nickname", nickname)
                // 设置签名,使用的加密算法以及密钥
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
        
        return jwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return false;
        }
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id,根据用户 id 查询数据库获取用户基本信息
     * @return
     */
    public static String getMemberIdByJwtToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return "";
        }
        Jws<Claims> claimsJws =
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String) claims.get("id");
    }

}
  1. 编写控制层,完成测试。为了方便测试,接口都为 GET 请求
@RestController
public class TestController {

    // 模拟登录请求
    @GetMapping("/login")
    public String token(String nickname, String password) {
        if ("abc".equals(nickname) && "123123".equals(password)) {
            String id = UUID.randomUUID().toString();
            System.out.println("生成的用户 id 为:" + id);
            return JwtUtil.getJwtToken(id, nickname);
        }
        return "";
    }

    // 从 token 中获取信息
    @GetMapping("/check")
    public String check(@RequestHeader(value = "token", required = false) String token) {
        String id = JwtUtil.getIdByJwtToken(token);
        System.out.println("该 token 的 id 为: " + id);
        return id;
    }
}
  1. 使用 Postman 完成测试,(项目使用的是 9999 端口)

首先发送一个登录请求:

image-20220710230620136.png

响应内容:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b2tlbi1kZW1vIiwiaWF0IjoxNjU3NDY1NTU5LCJleHAiOjE2NTc1NTE5NTksImlkIjoiMzY0YzMyMjYtYTU0MS00NTFiLWEwODYtMGMxNmM5NGRlMDM4Iiwibmlja25hbWUiOiJhYmMifQ._lRqiXNm9OtHeB8iP8rWSdGqdi1-EL2S5o4QFGV37gg

后端:

生成的用户 id 为:364c3226-a541-451b-a086-0c16c94de038

发送一个校验请求,通常情况下,前端发送 token 要么作为 url 的参数,要么设置在请求头里,笔者这里通过请求头获取 token,将得到 token 字符串设置在请求头里

image-20220710231004227.png

后端:

该 token 的 id 为: 364c3226-a541-451b-a086-0c16c94de038

再发送一个无效的token 字符串检测一遍:

image-20220710231115006.png

响应:

{
    "timestamp": "2022-07-10T15:10:58.602+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/check"
}

后端:

2022-07-10 23:10:58.596 ERROR 36844 --- [nio-9999-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: i�] with root cause

com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'i': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (String)"i�"; line: 1, column: 2]
 .............

总结

实际项目开发中,可以使用 JWT token 实现登录鉴权,特别是微服务架构中,不用考虑全局 session 的问题,只需要解析前端发送过来的 token 字符串是否有效即可。