eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoyLFwidXNlcm5hbWVcIjpcImxpc2lcIixcIm5hbWVcIjpcIuadjuWbm1wiLFwiZ3JhZGVcIjpcInZpcFwifSJ9.NT8QBdoK4S-PbnhS0msJAqL0FG2aruvlsBSyG226HiU这段加密字符串由三部分组成,中间由点“.”分隔,具体含义如下。
{ "alg": "HS256", "typ": "JWT" }然后,此 JSON 被 Base64 编码以形成 JWT 的第一部分。
{ "sub": "1234567890", "name": "John Doe", "admin": true }然后对原文进行 Base64 编码形成 JWT 的第二部分。
eyJzdWIiOiJ7XCJ1c2VySWRcIjoyLFwidXNlcm5hbWVcIjpcImxpc2lcIixcIm5hbWVcIjpcIuadjuWbm1wiLFwiZ3JhZGVcIjpcInZpcFwifSJ9第三部分 签名(Sign):签名就是通过前面两部分标头+载荷+私钥再配合指定的算法,生成用于校验 JWT 是否有效的特殊字符串,签名的生成规则如下。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)生成的签名字符串为:
NT8QBdoK4S-PbnhS0msJAqL0FG2aruvlsBSyG226HiU将以上三部分通过“.”连接在一起,就是 JWT 的标准格式了。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <!-- 堆代码 duidaima.com --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.2</version> <scope>runtime</scope> </dependency>第二步,编写创建 JWT 的测试用例,模拟真实环境 UserID 为 123 号的用户登录后的 JWT 生成过程。
@SpringBootTest public class JwtTestor { /** * 堆代码 duidaima.com * 创建Token */ @Test public void createJwt(){ //私钥字符串 String key = "1234567890_1234567890_1234567890"; //1.对秘钥做BASE64编码 String base64 = new BASE64Encoder().encode(key.getBytes()); //2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法 SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes()); //3.利用JJWT生成Token String data = "{\"userId\":123}"; //载荷数据 String jwt = Jwts.builder().setSubject(data).signWith(secretKey).compact(); System.out.println(jwt); } }运行结果产生 JWT 字符串如下:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxMjN9In0.1p_VTN46sukRJTYFxUg93CmfR3nJZRBm99ZK0e3d9Hw第三步,验签代码,从 JWT 中提取 123 号用户数据。这里要保证 JWT 字符串、key 私钥与生成时保持一致。否则就会抛出验签失败 JwtException。
/** * 堆代码 duidaima.com * 校验及提取JWT数据 */ @Test public void checkJwt(){ String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxMjN9In0.1p_VTN46sukRJTYFxUg93CmfR3nJZRBm99ZK0e3d9Hw"; //私钥 String key = "1234567890_1234567890_1234567890"; //1.对秘钥做BASE64编码 String base64 = new BASE64Encoder().encode(key.getBytes()); //2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法 SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes()); //3.验证Token try { //生成JWT解析器 JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build(); //解析JWT Jws<Claims> claimsJws = parser.parseClaimsJws(jwt); //得到载荷中的用户数据 String subject = claimsJws.getBody().getSubject(); System.out.println(subject); }catch (JwtException e){ //所有关于Jwt校验的异常都继承自JwtException System.out.println("Jwt校验失败"); e.printStackTrace(); } }运行结果如下:
{"userId":123}以上便是 JWT 的生成与校验代码,你会发现在加解密过程中,服务器私钥 key 是保障 JWT 安全的命脉。对于这个私钥在生产环境它不能写死在代码中,而是加密后保存在 Nacos 配置中心统一存储,同时定期更换私钥以防止关键信息泄露。讲到这应该你已掌握 JWT 的基本用法,但是在微服务架构下又该如何设计用户认证体系呢?
2.API 网关统一验签方案
{ "code": "0", "message": "success", "data": { "user": { "userId": 1, "username": "zhangsan", }, "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCIsXCJuYW1lXCI6XCLlvKDkuIlcIixcImdyYWRlXCI6XCJub3JtYWxcIn0ifQ.1HtfszarTxLrqPktDkzArTEc4ah5VO7QaOOJqmSeXEM" } }第四步,在收到上述 JSON 数据后,客户端将其中 token 数据保存在 cookie 或者本地缓存中;
{ "code": "0", "message": "success", "data": { "user": { #用户详细数据 "userId": 1, "username": "zhangsan", "name": "张三", "grade": "normal" "age": 18, "idno" : 130......., ... }, "authorization":{ #权限数据 "role" : "admin", "permissions" : [{"addUser","delUser","..."}] } } }第七步,具体的微服务收到上述 JSON 后,对当前执行的操作进行判断,检查是否拥有执行权限,权限检查通过执行业务代码,权限检查失败返回错误响应。到此从登录创建 JWT 到验签后执行业务代码的完整流程已经完成。
API 网关统一验签与服务端验签最大的区别是在 API 网关层面就发起 JWT 的验签请求,之后路由过程中附加的是从认证中心返回的用户与权限数据,其他的操作步骤与方案一是完全相同的。在这你可能又会有疑惑,为什么要设计两种不同的方案呢?其实这对应了不同的应用场景: