JWT
1. 介绍
JWT(JSON Web Token)是一种非常轻量级的规范;使我们能在用户和服务器之间传递安全可靠的消息。
JWT本质上是一个袖珍的安全令牌,允许我们以一种结构化的方式,在网络上安全的传输数据。
2. 构成
一个JWT实际上就是一个字符串,由三部分组合成:头部、载荷与签名。
1) 头部(header)是JWT的第一部分,通常是一个JSON对象;包含两个主要部分:令牌的类型(通常是JWT)和所使用的签名算法(比:HMAC SHA256或者RSA)。头部告诉接受方如何验证这个令牌。
{"typ":"JWT","alg":"HS256"}
下面有{"typ":"JWT","alg":"HS256"}编码后的结果:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
PS:表示JWT的类型是JWT,签名算法是HS256(HMAC SHA-256)。
编码后的字符串是通过将JSON对象 {"typ":"JWT","alg":"HS256"}
进行Base64 URL编码而得到的。在这个编码后的字符串中,.
被编码为 _
,+
被编码为 -
,/
被编码为 .
,这样可以确保编码后的字符串在URL中安全传输。
2) 载荷(Payload)是JWT的第二部分,也是一个JSON对象,包含一系列声明。这些声明是关于实体(如用户)及其他数据的声明。载荷是JWT中存储实际数据的部分,可以包含用户ID、角色、权限的数据。
存储有效信息的地方。包含三部分:
部分一:标准中注册的声明(建议但不强制,JWT规范默认的属性)
iss:JWT的签发者(issuer);
sub:当前令牌的描述说明(subject);
aud:接收JWT的一方(audience);
exp:JWT的过期时间,必须大于签发时间(expiration time);
nbf:定义在什么时间之前,该JWT都是不可用的(not before);
iat:JWT的签发时间(issued at);
jti:JWT的唯一身份标识,用于避免重放攻击(JWT ID);
部分二:公共的声明:可以添加任何信息。通常用于添加用户的相关信息和其他业务需求的必要信息。但不建议添加敏感信息,因为信息可在客户端解密。
部分三:私有的声明:私有声明是提供者和消费者共同定义的声明,通常用于自定义信息。一般不建议存放敏感信息,因为base64是对称解密,意味着该部分信息可归类为明文信息。
与标准声明不同的是,私有声明不会被验证,除非明确告知接收方要对这些声明要进行验证和验证规则。
这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。
这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,
都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
{"sub":"1234567890","name":"John Doe","admin":true}
base64加密结果是:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
载荷中只有标准声明是参与校验的,公共和私有是不参与校验的。
3) 签名(Signature)是JWT的第三部分,由头部、载荷经过Base64编码后的字符串,及一个私钥或盐(secret)共同组成。签名用于验证JWT的真实性和完整性,确保令牌在传输过程中没有被篡改。
签名 = {base64(header) + base64(payload) + 私钥(盐) } +HS256算法;
3. JWT代码
1) 新建一个boot项目
导入依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
<!-- javax.xml.bind.DatatypeConverter 在Java9 版本后加 ->--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <!-- 或 ->--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
javax.xml.bind:jaxb-api
是 Java Architecture for XML Binding (JAXB) API 的依赖。JAXB 是 Java 中用于 XML 数据与 Java 类之间相互转换的标准 API。
若不报错可不加。
2) 加密&解密测试
// 加密测试 @Test void encryption() { JwtBuilder builder= Jwts.builder() // 用于生成JWT .setId("2024") // 设置唯一编号 .setSubject("JWT") // 设置主题 可以是JSON数据 .setIssuedAt(new Date()) // 设置签发日期 .setExpiration(new Date(new Date().getTime() + 7 * 24 * 3600 * 1000L)) // 过期时间 当前时间 + 七天的毫秒数 .signWith(SignatureAlgorithm.HS256,"buildJWT"); // 设置签名 使用HS256算法,并设置SecretKey(字符串) // 构建 并返回一个字符串 System.out.println("结果:" + builder.compact() ); // eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMDI0Iiwic3ViIjoiSldUIiwiaWF0IjoxNzA4Njk1NTM3LCJleHAiOjE3MDkzMDAzMzd9.CB37ieSRyB0309Ywdv8F7LktR_AzgVGWee7fw23j9t0
// 头部(token类型、签名加密方式).载荷.签名(包含算法)
} // 解密测试 @Test public void decrypt(){ String compactJwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMDI0Iiwic3ViIjoiSldUIiwiaWF0IjoxNzA4Njk1NTM3LCJleHAiOjE3MDkzMDAzMzd9.CB37ieSRyB0309Ywdv8F7LktR_AzgVGWee7fw23j9t0"; Claims claims = Jwts.parser(). setSigningKey("buildJWT"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims); // {jti=2024, sub=JWT, iat=1708695537, exp=1709300337} }
3) 生成工具类
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; public class JwtUtil { // token 有效期 public static final Long JWT_TTL = 3600000L; // 60 * 60 * 1000 一个小时 // Jwt令牌信息 public static final String JWT_KEY = "Java_JWT"; // 创建token方法 public static String createJWT(String id, String subject, Long ttlMillis) { // id, 信息, 过期时间 // 指定算法 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 当前系统时间 long nowMillis = System.currentTimeMillis(); //令牌签发时间 Date now = new Date(nowMillis); // 如果令牌有效期为null,则默认设置有效期1小时 if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; } // 令牌过期时间设置 long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); // 生成秘钥 SecretKey secretKey = generalKey(); // 封装Jwt令牌信息 JwtBuilder builder = Jwts.builder() .setId(id) // 唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("admin") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙 .setExpiration(expDate); // 设置过期时间 // builder.addClaims(map); 可以放一些额外信息; return builder.compact(); } /** * 生成加密 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); // 转为字节码 SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析令牌数据 * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
4) 测试
public static void main(String[] args) { // 加密 String encryptionToken = JwtUtil.createJWT("007", "学Java的bei", 7 * 24 * 3600 * 1000L); System.out.println("加密:" + encryptionToken); // 加密:eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDciLCJzdWIiOiLlraZKYXZh55qEYmVpIiwiaXNzIjoiYWRtaW4iLCJpYXQiOjE3MDg2OTc5MDEsImV4cCI6MTcwOTMwMjcwMX0.W0D-u0KcIG1Uiz8WN12s0xYR1f3abdzeM5iFrHd8umc System.out.println("·····································································"); // 解密 String decryptToken = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDciLCJzdWIiOiLlraZKYXZh55qEYmVpIiwiaXNzIjoiYWRtaW4iLCJpYXQiOjE3MDg2OTc0MjUsImV4cCI6MTcwOTMwMjIyNX0.HxE5aoGLAPxXJIjdcEynJ6d1v_KAwnC0I4G7R89YDKw"; try { System.out.println("解密:" + parseJWT(decryptToken)); } catch (Exception e) { throw new RuntimeException(e); } // 解密:{jti=007, sub=学Java的bei, iss=admin, iat=1708697425, exp=1709302225} }
4. 注册、登录
1) 注册:使用用户名正则和密码正则,判断用户名格式是否通过,用户名是否被注册;判断密码格式是否通过并通过MD5加密。
public Result register(User user) { // 判断用户名格式与是否被注册 String username = user.getUsername(); // 判断用户名是否被注册 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); if (userMapper.selectOne(queryWrapper) != null) { return Result.error("此用户名已被注册."); } // 判断用户名格式 if (!RegexUtil.checkUsername(username)) { return Result.error("用户名格式有误."); } // 判断密码格式 String userPassword = user.getPassword(); if (!RegexUtil.checkPassword(userPassword)) { return Result.error("密码格式有误."); } // 密码加密 user.setPassword(MD5Util.MD5Encode(user.getPassword(),"utf-8")); int insertUser = userMapper.insert(user); return insertUser > 0 ? Result.ok("注册成功.") : Result.error("注册失败."); }
MD5:https://www.cnblogs.com/warmNest-llb/p/18031009
正则:https://www.cnblogs.com/warmNest-llb/p/18031247
2) 登录
未生成JWT(Token)前:
/** * 登录 * @param username * @param password * @return */ @Override public Result login(String username, String password) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); queryWrapper.eq("password", MD5Util.MD5Encode(password,"utf-8")); User userMatch = userMapper.selectOne(queryWrapper); return userMatch != null ? Result.ok("登录成功.") : Result.error("用户名或密码有误."); }
生成JWT(Token)后:
/** * 登录 * @param username * @param password * @return */ @Override public Result login(String username, String password) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); queryWrapper.eq("password", MD5Util.MD5Encode(password,"utf-8")); User userMatch = userMapper.selectOne(queryWrapper); // 加上JWT(token) userMatch.setPassword(null); if (userMatch != null) { String jsonString = JSON.toJSONString(userMatch); String token = JwtUtil.createJWT(userMatch.getId().toString(), jsonString, 7 * 24 * 2600 * 1000L); return Result.ok(token); } return Result.error("用户名或密码有误."); }
5. JWT的工作原理
基于JWT的认证和授权机制,通过在用户登录后生成并返回JWT,在每次请求中携带JWT,并在服务器端验证和解析JWT来实现用户身份认证和授权。
简单说:登录时等成JWT(token),返回给前端,前端拿到后放到请求头,前端的每次请求都可以带token到请求头;判断是否带上token,并解析(带-进,不带-禁);每次访问都由拦截器拦截。
具体看 JWT整合Spring Boot:https://www.cnblogs.com/warmNest-llb/p/18031456
token、jwt 和 jwt刷新token:https://www.cnblogs.com/warmNest-llb/p/18108105