前言
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
使用
- 新建一个 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>
- 新建一个工具类,负责处理 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");
}
}
- 编写控制层,完成测试。为了方便测试,接口都为 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;
}
}
- 使用 Postman 完成测试,(项目使用的是 9999 端口)
首先发送一个登录请求:
响应内容:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b2tlbi1kZW1vIiwiaWF0IjoxNjU3NDY1NTU5LCJleHAiOjE2NTc1NTE5NTksImlkIjoiMzY0YzMyMjYtYTU0MS00NTFiLWEwODYtMGMxNmM5NGRlMDM4Iiwibmlja25hbWUiOiJhYmMifQ._lRqiXNm9OtHeB8iP8rWSdGqdi1-EL2S5o4QFGV37gg
后端:
生成的用户 id 为:364c3226-a541-451b-a086-0c16c94de038
发送一个校验请求,通常情况下,前端发送 token 要么作为 url 的参数,要么设置在请求头里,笔者这里通过请求头获取 token,将得到 token 字符串设置在请求头里
后端:
该 token 的 id 为: 364c3226-a541-451b-a086-0c16c94de038
再发送一个无效的token 字符串检测一遍:
响应:
{
"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 字符串是否有效即可。