文章目录
- 使用JWT的原因
- JWT结构
- JWT入门案例
- Token拦截
使用JWT的原因
为了保护项目之中的数据资源,那么一定就需要采用认证检测机制,于是SpringCloud进行认证处理,就可以使用SpringSecurity 来实现了,但是如果你真的去使用了SpringSecurity进行开发,因为维护的成本实在是太高了。
在后来的时候有很多的开发者开始尝试通过OAuth2统一认证来进行SpringCloud认证与授权服务,这种操作也属于较早期的实现了,这种实现最大的问题在于随着版本的更新会出现代码不稳定的情况,很多的开发者就开始尝试自己去独立的实现认证与授权的操作机制,于是就有了JWT开发技术,使用JWT最大的特点在于不需要维护所有的数据的状态。
使用JWT最大的特点在于不需要维护所有的数据的状态,同时可以自己包含有过期的时间,以及通过附加数据的形式传送所需要的额外的数据内容((可以保存认证以及授权信息)。
在WEB开发中需要维护Session的状态,所以如果用户访问量过大,那么必然会出现Session内容过多而导致服务器处理性能下降的惨剧,所以后面就需要引入WEB服务器的集群,但是为了便于服务器集群之中的Session管理,那么又需要进行分布式的Session存储,总之就一点:有状态的用户需要维护Session,Session维护成本很高。
对于JWT需要提供有一个专属的开发组件,这些组件可以实现JWT数据的生成以及各种检测操作机制,最重要的一点所有的认证可以直接通过网关来进行过滤,可以考虑通过网关来进行认证状态的检查,而后每一个具体的微服务实现权限的排查。
JWT结构
基于Token的单点登录技术可以有效的节约开发成本,但是这里面最重要的一点就是JWT数据的组成结构定什么?在实际的项目开发中,JWT主要是为了实现用户认证数据的处理,所以第三方应用客户端要想进行用户统一登录的操作,只需要传入用户认证所需要的数据信息,即可成功的获取到Token令牌,考虑到令牌的安全性以及实用性,在每一个JWT数据中会包含有三类信息项: Header头部信息、Payload负载信息、Signature数字签名。
使用JWT的结构特点,可以有效的实现用户数据信息的携带,每次进行服务调用时都需要传递此JWT数据,目标微服务依靠此数据实现用户登录状态检测,同时也可以根据其保存的用户角色数据,来进行当前操作执行的合法性校验
JWT入门案例
在实际项目开发中,不同的项目会存在有不同的数字签名、发布者等数据信息,考虑到JWT使用的便捷性,可以直接通过application.yml配置JWT的相关属性内容,随后将这些属性注入到指定的配置类中
1、【microboot项目】创建一个新的子模块“microboot-jwt”子模块,随后修改build.gradle配置文件添加依赖:
dependencies.gradle
ext.versions = [jjwt : '0.9.1', // JWT依赖库jaxb : '2.3.1', // JAXB依赖库 JDK11需要加这个依赖,8不需要
]
ext.libraries = [// 以下的配置为JWT所需要的依赖库'jjwt': "io.jsonwebtoken:jjwt:${versions.jjwt}",'jaxb-api': "javax.xml.bind:jaxb-api:${versions.jaxb}"
]
build.gradle
project('microboot-jwt') { // 子模块dependencies { // 配置子模块依赖compile('org.springframework.boot:spring-boot-starter-web')compile(libraries.'fastjson')compile(libraries.'jjwt')compile(libraries.'jaxb-api')}
}
2、【microboot-jwt子模块】创建 application.yml配置文件,配置JWT的相关环境属性,同时考虑到了很多的信息需要自定义,同时也需要保存有应用的名称,所以要添加“spring.application.name”配置项
spring:application:name: microboot-jwt # 应用名称
muyan: # 自定义配置项config: # 配置项定义jwt: # 配置JWT相关属性sign: muyan # JWT证书签名issuer: MuyanYootk # 证书签发人secret: www.yootk.com # 加密密钥expire: 10 # 有效时间(单位:秒)
如果你现在要做的是一个非常大型的JWT系统,那么这个时候可以考虑通过数据库来实现以上的配置项
3、【microboot-jwt子模块】所有的配置项一定要被程序进行读取,可以创建有一个配置类
package com.yootk.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "muyan.config.jwt")
public class JWTConfigProperties { // 保存JWT配置项private String sign;private String issuer;private String secret;private long expire;
}
在之前application.yml中所配置的相关属性,实际上都是为了JWT数据服务准备的,因为JWT在整个的项目设计之中是一个核心的结构,所以最佳的做法是通过一个服务的包装类,来包裹jjwt依赖库里面所提供的各个组件。
4、【microboot-jwt子模块】既然要进行服务的开发,首先就应该要提供有一个完善的业务接口。
package com.yootk.service;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;import javax.crypto.SecretKey;
import java.util.Map;public interface ITokenService { // 实现JWT的相关操作接口public SecretKey generalKey(); // 获取当前JWT数据加密KEY/*** 生成一个合法的Token数据* @param id 这个Token的唯一ID(随意存储,本次可以考虑存储用户ID)* @param subject 所有附加的信息内容,本次直接接收了一个Map,但是最终存储的时候存放JSON* @return 返回一个有效的Token数据字符串*/public String createToken(String id, Map<String, Object> subject);/*** 是根据Token的字符串内容解析出其组成的信息(头信息与附加信息)* @param token 要解析的Token完整数据* @return Jws接口实例* @throws JwtException 如果Token失效或者结构错误*/public Jws<Claims> parseToken(String token) throws JwtException;/*** 校验当前传递的Token数据是否正确* @param token 要检查的Token数据* @return true表示合法、false表示无效*/public boolean verifyToken(String token);/*** Token存在有效时间的定义,所以一定要提供有Token刷新机制* @param token 原始的Token数据* @return 新的Token数据*/public String refreshToken(String token);
}
5、【microboot-jwt子模块】定义ITokenService业务接口子类,并且完成所有的抽象方法
package com.yootk.service.impl;import com.alibaba.fastjson.JSONObject;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Service
public class TokenServiceImpl implements ITokenService {@Autowiredprivate JWTConfigProperties jwtConfigProperties; // JWT的相关配置属性@Value("${spring.application.name}")private String applicationName;private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法@Overridepublic SecretKey generalKey() { // 获取加密KEYbyte[] encodedKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}@Overridepublic String createToken(String id, Map<String, Object> subject) {Date nowDate = new Date(); // 获取当前的日期时间// 当前的时间 + 失效时间配置的秒数 = 最终失效的日期时间Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 10000);Map<String, Object> claims = new HashMap<>(); // 附加的Claims信息claims.put("site", "www.yootk.com"); // 添加信息内容claims.put("book", "SpringBoot就业编程实战"); // 添加信息内容claims.put("company", "沐言科技"); // 添加信息内容Map<String, Object> headers = new HashMap<>(); // 保存的头信息headers.put("author", "李老师");headers.put("module", this.applicationName); // 保存应用的名称headers.put("desc", "喜欢你。");JwtBuilder builder = Jwts.builder().setClaims(claims) // 保存Claims信息.setHeader(headers) // 保存Headedr信息.setId(id) // 保存ID内容.setIssuedAt(nowDate) // 证书签发日期时间.setIssuer(this.jwtConfigProperties.getIssuer()) // 证书签发者.setSubject(JSONObject.toJSONString(subject)) // 附加信息.signWith(this.signatureAlgorithm, this.generalKey()) // 签名算法.setExpiration(expireDate); // Token失效时间return builder.compact(); // 生成Token}@Overridepublic Jws<Claims> parseToken(String token) throws JwtException {if (this.verifyToken(token)) { // 检查当前的Token是否正确Jws<Claims> claims = Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token);return claims;}return null;}@Overridepublic boolean verifyToken(String token) {try {Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();return true;// 没有异常,解析成功} catch (JwtException exception) {return false;}}@Overridepublic String refreshToken(String token) {if (this.verifyToken(token)) { // 正确的Token是可以进行刷新的Jws<Claims> claimsJws = this.parseToken(token); // 解析数据return this.createToken(claimsJws.getBody().getId(), JSONObject.parseObject(claimsJws.getBody().getSubject(), Map.class));}return null;}
}
6、【测试】
package com.yootk.test;import com.alibaba.fastjson.JSONObject;
import com.yootk.StartJWTApplication;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;import java.util.HashMap;
import java.util.Map;
import java.util.UUID;@ExtendWith(SpringExtension.class) // Junit5测试工具
@WebAppConfiguration // 表示需要启动Web配置才可以进行测试
@SpringBootTest(classes = StartJWTApplication.class) // 定义要测试的启动类
public class TestTokenService {@Autowiredprivate ITokenService tokenService;private String token = "eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3Njk5NzgsImlhdCI6MTYyNDc1OTk3OCwianRpIjoieW9vdGstNGQ2YzdkMzItZmE5Mi00ZTc4LWJkN2YtNzE1MGMxMDA3MDRlIn0.B7f11ckb4etMTcxzdzTh_1VubQSHnifl43t2-3atrD4";@Testpublic void testCreate() { // 创建Token数据Map<String, Object> map = new HashMap<>(); // 保存subject数据信息map.put("mid", "muyan");map.put("name", "沐言科技-李兴华");map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 保存角色数据String id = "yootk-" + UUID.randomUUID(); // 随机生成IDSystem.out.println(this.tokenService.createToken(id, map));}@Testpublic void testParse() {Jws<Claims> claims = this.tokenService.parseToken(token); // 解析得到的Token数据claims.getHeader().forEach((name, value) -> {System.out.println("【JWT头信息】name = " + name + "、value = " + value);});System.err.println("------------------------------------------------------------------");claims.getBody().forEach((name, value) -> {System.out.println("【JWT主题信息】name = " + name + "、value = " + value);});System.err.println("------------------------------------------------------------------");Map<String, Object> map = JSONObject.parseObject(claims.getBody().get("sub").toString(), Map.class); // 用户配置的信息map.entrySet().forEach(entry -> {System.out.println("【用户数据】key = " + entry.getKey() + "、value = " + entry.getValue());});}@Testpublic void testVerifyJWT() {System.out.println(this.tokenService.verifyToken(token));}@Testpublic void testRefreshToken() {System.out.println(this.tokenService.refreshToken(token));}
}
Token拦截
本次的Token主要是为了保护最终微服务的资源的,但是所有的资源保护都有一个基个前提,那么就是要有相应的拦截处理,也就是说可以通过拦截器的形式来进行资源的保护,但是毕竟有些资源是需要保护的,而有一些资源是需要保护的,此时可以考虑通过一个自定义注解的形式来区分保护与非保护资源。
1、【microboot-jwt子模块】创建一个用于区分是否要使用Token保护的注解
package com.yootk.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD}) // 该注解主要用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface JWTCheckToken { // JWT的检查注解public boolean required() default true; // 是否要启用Token检查
}
2、【microboot-jwt子模块】为了便于该注解的使用,可以创建一个MessageAction程序类,并且进行信息响应
package com.yootk.action;import com.yootk.annotation.JWTCheckToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/message/*") // 父路径
public class MessageAction {@RequestMapping("echo")@JWTCheckToken // 这个资源需要被检查public Object echo(String msg) {return "【ECHO】" + msg;}
}
3、【microboot-jwt子模块】如果最终需要此注解生效,那么就需要定义拦截器。
package com.yootk.interceptor;import com.yootk.annotation.JWTCheckToken;
import com.yootk.service.ITokenService;
import jdk.jfr.Frequency;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;public class JWTAuthenticationInterceptor implements HandlerInterceptor { // 认证拦截器// Token可以通过参数传递也可以通过头信息传递private static final String TOKEN_NAME = "yootkToken"; // Token参数名称@Autowiredprivate ITokenService tokenService; // Token业务接口@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) { // 不处理拦截操作return true;}HandlerMethod handlerMethod = (HandlerMethod) handler; // 类型转换Method method = handlerMethod.getMethod(); // 获取当前要执行的Action方法反射对象if (method.isAnnotationPresent(JWTCheckToken.class)) { // 判断该方法上是否提供有指定的注解JWTCheckToken checkToken = method.getAnnotation(JWTCheckToken.class); // 获取指定注解if (checkToken.required()) { // true表示要进行Token检查String token = this.getToken(request); // 获取Token数据if (!this.tokenService.verifyToken(token)) { // 验证失败throw new RuntimeException("Token数据无效,无法访问。");}}}return true;}public String getToken(HttpServletRequest request) {String token = request.getParameter(TOKEN_NAME); // 通过参数获取头信息if (token == null || "".equals(token)) { // 没有接收到Tokentoken = request.getHeader(TOKEN_NAME); // 通过头信息获取}return token;}
}
4、【microboot-jwt子模块】如果要想让拦截器生效,则需要定义一个配置类。
package com.yootk.config;import com.yootk.interceptor.JWTAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer { // 拦截配置类@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.getJWTAuthenticationInterceptor()).addPathPatterns("/**");}@Beanpublic HandlerInterceptor getJWTAuthenticationInterceptor() {return new JWTAuthenticationInterceptor();}
}
5、【浏览器】直接通过浏览器进行访问
localhost:8080/message/echo?msg=www.yootk.com&yootkToken=eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3NzE1MTYsImlhdCI6MTYyNDc2MTUxNiwianRpIjoieW9vdGstZGJlZDhhMmYtMjBhYi00Njg1LWI2ZDktNmEzNzI0Y2RhZDZiIn0.DDpaXGOdTYDlUZgOMB59uVlgYTZogDYjLW-pvNuvizE
此时通过JWT数据实现的认证检查处理,要比之前使用SpringSecurity、OAuth2、Shiro都简单许多,一个字符串就可以轻松搞定所有的问题了。