Jwt 令牌
1、JWT 基础介绍
1)What Is JWT?
- JWT(JSON Web Token),是一种开放的标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全的传递信息,是一种轻量级的、自包含的、可验证的令牌,通常用于
身份验证
和授权机制
。 - 其中包含了有关用户或者实体的信息,数字签名或者加密信息,来保证令牌的完整性和安全性。
2)Why JWT?
首先来看一下同样可以用于身份验证的 session 的实现原理
既然有了 session 就能实现对用户的验证,那我们为什么还需要 JWT 技术呢?
来看一下 session 技术的一些缺点
- 使用会话技术需要在服务器端来维护会话的状态,这意味着服务器需要分配内存来存储会话的数据,当应用的规模很大的适合,这样会导致服务器的负载增加。
- 会话技术使用服务器存储会话状态的性质,导致了在扩展应用方面引起的问题,需要考虑如何共享会话状态,实现负载均衡和扩展性。
- 同时读写会话数据引入性能开销,尤其是在高负载的应用中,等等。
比起 session 会话技术,jwt 有以下的优势
- JWT 是无状态的,所有的必要信息都存储子啊令牌中,不需要在服务端维护会话状态中。
- JWT 可以用于在不同域之间来安全的传递用户身份信息
- JWT 可以使用数字签名或者加密来验证令牌的完整性和保密性,防止数据的篡改和伪造。
- JWT 的标准规范不限制用途,可以包含任何需要的信息。
2、JWT 认证
JWT 主要是由三部分组成的:Header 标头、Payload 负载、Signature 签名
1)JWT的结构
Header
标头通常包含了令牌的类型(JWT)和所使用的签名算法(例如,HMAC SHA256或RSA),以及其他元数据。
{
"alg": "HS256", // 签名算法,例如使用 HMAC SHA-256
"typ": "JWT" // 令牌的类型
}
Payload
负载包含了有关用户或实体的声明,这些声明可以是标准声明(例如,身份验证时间,到期时间等)或自定义声明(根据需要添加的任何信息)。
{
"sub": "1234567890", // 主题(Subject),通常是用户的唯一标识
"name": "John Doe", // 用户姓名
"iat": 1516239022 // 签发时间(Issued At),以 UNIX 时间戳表示
}
Signature
签名用于验证令牌的完整性。签名算法是在标头中指定的,通常使用密钥来生成签名。只有拥有正确密钥的接收方才能验证签名并确定令牌是否有效。
//签名部分用于验证令牌的完整性,通常使用密钥和指定的签名算法进行签名。签名的内容是基于头部和负载的编码字符串。
完整的 jwt 令牌的示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.sNCC5z6JBi0kjJvIzl3pJR5CfFmm9Xmp9_X9t_Y4Lrw
2)JWT 认证的工作流程
- 前端通过表单将自己的信息发送到后端的接口上。
- 后端接收到这些信息之后,将这些信息作为 JWT 的负载(Payload),将其和头部进行编码拼接和签名,形成一个 JWT 密钥(Token)。
- 服务器将生成的这个 JWT 作为响应的一部分返回给客户端。
- 客户端收到 JWT 并在后续请求中将其包含在请求头或其他适当位置中。
- 服务器在接收到请求后,验证 JWT 的签名以确保其完整性和真实性。
- 如果 JWT 有效,服务器使用其中的信息来授权用户执行请求的操作。
3、使用 JWT
1)如何在 Java 程序中使用 JWT 认证?
引入依赖
<properties>
<jjwt>0.9.1</jjwt>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt}</version>
</dependency>
这里我们引入 jjwt(Java JWT: JSON Web Token for Java and Android) 依赖来实现在 Java 中使用 JWT,
示例代码
因为解析和创建 jwt 的操作是被经常使用的,为了避免代码的重复将其封装成了 Util 类
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
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
*/
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;
}
登录功能的实现
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成 jwt 令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
jwt 校验的拦截器
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @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.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id{}", empId);
//3、通过,放行
BaseContext.setCurrentId(empId);
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
这样就实现了 jwt 在网络层的一个基本应用
JwtBulider 中可以设置的属性
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package io.jsonwebtoken;
import java.security.Key;
import java.util.Date;
import java.util.Map;
public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
JwtBuilder setHeader(Header var1);
JwtBuilder setHeader(Map<String, Object> var1);
JwtBuilder setHeaderParams(Map<String, Object> var1);
JwtBuilder setHeaderParam(String var1, Object var2);
JwtBuilder setPayload(String var1);
JwtBuilder setClaims(Claims var1);
JwtBuilder setClaims(Map<String, Object> var1);
JwtBuilder addClaims(Map<String, Object> var1);
JwtBuilder setIssuer(String var1);
JwtBuilder setSubject(String var1);
JwtBuilder setAudience(String var1);
JwtBuilder setExpiration(Date var1);
JwtBuilder setNotBefore(Date var1);
JwtBuilder setIssuedAt(Date var1);
JwtBuilder setId(String var1);
JwtBuilder claim(String var1, Object var2);
JwtBuilder signWith(SignatureAlgorithm var1, byte[] var2);
JwtBuilder signWith(SignatureAlgorithm var1, String var2);
JwtBuilder signWith(SignatureAlgorithm var1, Key var2);
JwtBuilder compressWith(CompressionCodec var1);
String compact();
}