JWT:Json Web Token
Token上可能需要记录用户身份的多项数据,例如id、username等,这些数据应该被有效的组织起来,以至于后续服务器端能够验证真伪,并解析出其中的数据,使用JWT时,这些数据是使用JSON格式组织起来的。
关于JWT的使用,有一套固定的标准,它约定了JWT数据的组成部分,必须包含:
- 头部信息(Header)
- 载荷(Payload):数据
- 数据签名(Signature)
使用JWT
首先,需要添加相关的依赖项,推荐的依赖项有:
<!-- JJWT(Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT和解析JWT的示例(测试)代码如下:
package cn.tedu.csmall.passport;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTests {
// 是一个自定义的字符串,应该是一个保密数据,最低要求不少于4个字符,但推荐使用更加复杂的字符串
String secretKey = "fdsFOj4tp9Dgvfd9t45rDkFSLKgfR8ou";
@Test
void generate() {
// JWT的过期时间
Date date = new Date(System.currentTimeMillis() + 5 * 60 * 1000);
// 你要存入到JWT中的数据
Map<String, Object> claims = new HashMap<>();
claims.put("id", 9527);
claims.put("username", "test-jwt");
claims.put("phone", "13800138001");
String jwt = Jwts.builder() // 获取JwtBuilder,准备构建JWT数据
// 【1】Header:主要配置alg(algorithm:算法)和typ(type:类型)属性
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// 【2】Payload:主要配置Claims,把你要存入的数据放进去
.setClaims(claims)
// 【3】Signature:主要配置JWT的过期时间、签名的算法和secretKey
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256, secretKey)
// 完成
.compact(); // 得到JWT数据
System.out.println(jwt);
}
@Test
void parse() {
// 需要被解析的JWT,在复制此数据时,切记不要多复制了换行符(\n)
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwaG9uZSI6IjEzODAwMTM4MDAxIiwiaWQiOjk1MjcsImV4cCI6MTY3MTA5NTI3MiwidXNlcm5hbWUiOiJ0ZXN0LWp3dCJ9.9aHPOE-JLjCqd9sKEehoZzqGhz7hpsYcUwIzpiVdfmg";
// 执行解析
Claims claims = Jwts.parser() // 获得JWT解析工具
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
// 从Claims中获取生成时存入的数据
Object id = claims.get("id");
Object username = claims.get("username");
Object phone = claims.get("phone");
System.out.println("id = " + id);
System.out.println("username = " + username);
System.out.println("phone = " + phone);
}
}
当尝试解析JWT时,如果JWT已经过期,则会出现io.jsonwebtoken.ExpiredJwtException:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-12-15T17:07:52Z. Current time: 2022-12-15T17:25:22Z, a difference of 1050481 milliseconds. Allowed clock skew: 0 milliseconds.
当尝试解析JWT时,如果验证签名失败,则会出现io.jsonwebtoken.SignatureException:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
当尝试解析JWT时,如果因为JWT数据有误导致解析失败,则会出现io.jsonwebtoken.MalformedJwtException:
io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"phone":"13800138001","id":9527,"exp":16%rname":"test-jwt"}
注意:不要在JWT中存入敏感信息,或核心数据,在不知道secretKey的情况下,依然可以根据JWT解析出相关数据,只是签名验证失败而已!例如,你可以将生成好的JWT粘贴到JWT官网上,可以看到JWT将解析出Claims中的信息,但是,会提示验证签名失败!所以,对于验证签名失败的JWT应该视为“错误的”,或“不可信任的”。
登录成功后响应JWT
首先,需要修改IAdminService中login()方法的返回值类型,由void改为String,表示认证通过后将返回JWT数据:
/**
* 管理员登录
*
* @param adminLoginDTO 封装了登录参数的对象
* @return 管理员登录成功后将得到的JWT
*/
String login(AdminLoginDTO adminLoginDTO);
并且,修改AdminServiceImpl中重写的方法:
@Override
public String login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 执行认证
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
Authentication authenticateResult
= authenticationManager.authenticate(authentication);
log.debug("认证通过!");
log.debug("认证结果:{}", authenticateResult); // 注意:此认证结果中的Principal就是UserDetailsServiceImpl中返回的UserDetails对象
// 从认证结果中取出将要存入到JWT中的数据
Object principal = authenticateResult.getPrincipal();
AdminDetails adminDetails = (AdminDetails) principal;
Long id = adminDetails.getId();
String username = adminDetails.getUsername();
// 将认证通过后得到的认证信息存入到SecurityContext中
// 【注意】注释以下2行代码后,在未完成JWT验证流程之前,用户的登录将不可用
// SecurityContext securityContext = SecurityContextHolder.getContext();
// securityContext.setAuthentication(authenticateResult);
// ===== 生成并返回JWT =====
// 是一个自定义的字符串,应该是一个保密数据,最低要求不少于4个字符,但推荐使用更加复杂的字符串
String secretKey = "fdsFOj4tp9Dgvfd9t45rDkFSLKgfR8ou";
// JWT的过期时间
Date date = new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000);
// 你要存入到JWT中的数据
Map<String, Object> claims = new HashMap<>();
claims.put("id", id);
claims.put("username", username);
// claims.put("权限", "???"); // TODO 待处理
String jwt = Jwts.builder() // 获取JwtBuilder,准备构建JWT数据
// 【1】Header:主要配置alg(algorithm:算法)和typ(type:类型)属性
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// 【2】Payload:主要配置Claims,把你要存入的数据放进去
.setClaims(claims)
// 【3】Signature:主要配置JWT的过期时间、签名的算法和secretKey
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256, secretKey)
// 完成
.compact(); // 得到JWT数据
log.debug("即将返回JWT数据:{}", jwt);
return jwt;
}
完成后,可以在AdminServiceTests中测试,当登录成功后,在日志中可以看到JWT数据:
@Test
void login() {
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername("liucangsong");
adminLoginDTO.setPassword("123456");
try {
String jwt = service.login(adminLoginDTO);
log.debug("登录成功,JWT:{}", jwt);
} catch (Throwable e) {
// 由于不确定Spring Security会抛出什么类型的异常
// 所以,捕获的是Throwable
// 并且,在处理时,应该打印信息,以了解什么情况下会出现哪种异常
e.printStackTrace();
}
}
以上测试时在控制台中输出的JWT,可以在JwtTests测试类中成功解析(注意:使用相同的secretKey)。
完成后,调整AdminController中处理登录的请求,当登录成功后,向客户端响应JWT数据:
// http://localhost:9081/admins/login
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
// ↓↓↓↓↓↓↓ 获取调用方法的返回结果,即JWT数据
String jwt = adminService.login(adminLoginDTO);
// ↓↓↓ 将JWT封装到响应对象中
return JsonResult.ok(jwt);
}
完成后,重启项目,通过Knife4j的API文档进行调试,当登录成功后,将响应JWT数据。