JWT(JSON Web Token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。javascript
header前端
jwt的头部承载两部分信息:java
{ 'typ': 'JWT', //声明类型 'alg': 'RS256' //签名加密的算法 }
而后将头部进行base64加密(该加密是能够对称解密的),构成了第一部分.git
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playloadgithub
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:web
{ "iss": "JWT Builder", //jwt签发者 "iat": 1416797419, // jwt的签发时间 "exp": 1448333419, //jwt的过时时间,这个过时时间必需要大于签发时间 "aud": "www.bilibili.com", //接收jwt的一方 "sub": "1837307557@qq.com", //jwt所面向的用户 "GivenName": "Levin", "Surname": "Levin", "Email": "1837307557@qq.com", "Role": [ "ADMIN", "MEMBER" ], "nbf" : 1416797420 //定义在什么时间以前,该jwt都是不可用的, "jti" : "jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击" }
定义一个payload:redis
// 包括须要传递的用户信息; { "iss": "Online JWT Builder", "iat": 1416797419, "exp": 1448333419, "aud": "www.gusibi.com", "sub": "uid", "nickname": "goodspeed", "username": "goodspeed", "scopes": [ "admin", "user" ] }
而后将其进行base64加密,获得Jwt的第二部分。算法
eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVk
signaturespring
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:数据库
// 根据头部alg算法与私有秘钥进行加密获得的签名字符串; // 这一段是最重要的敏感信息,只能在服务端解密; HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECREATE_KEY )
这个部分须要base64加密后的header和base64加密后的payload使用 "." 链接组成的字符串,而后经过header中声明的加密方式进行加盐secret组合加密,而后就构成了jwt的第三部分。
// javascript var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用"."链接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,因此,它就是你服务端的私钥,在任何场景都不该该流露出去。一旦客户端得知这个secret, 那就意味着客户端是能够自我签发jwt了。
加密:
生成头JSON,荷载(playload) JSON
将头JSON Base64编码 + 荷载JSON Base64编码 +secret 三者拼接进行加密获得签名
JSON Base64编码 + 荷载JSON Base64编码 + 签名 三者经过 "." 相链接
一条 hhh.ppp.sss 格式的JWT 即生成
解密:
取得Jwt hhh.ppp.sss 格式字符,经过 "." 将字符分为三段
对第一段进行Base64解析获得header json,获取加密算法类型
将第一段Header JSON Base64编码 + 第二段 荷载JSON Base64编码 + secret采用相应的加密算法加密获得签名
将步骤三获得的签名与步骤一分红的第三段也就是客户端传入的签名进行匹配,匹配成功说明该jwt为server自身产出;
获取playload内信息,经过信息能够作鉴权操做;
成功访问;
经过这些步骤,保证了第三方没法修改jwt,jwt只能自产自销,在分布式环境下服务接收到合法的jwt即可知是本系统内自身或其余服务发出的jwt,该用户是合法的;
X509
X.509是常见通用的证书格式。全部的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。X.509是国际电信联盟-电信(ITU-T)部分标准和国际标准化组织(ISO)的证书格式标准。做为ITU-ISO目录服务系列标准的一部分,X.509是定义了公钥证书结构的基本标准。1988年首次发布,1993年和1996年两次修订。当前使用的版本是X.509 V3,它加入了扩展字段支持,这极大地增进了证书的灵活性。X.509 V3证书包括一组按预约义顺序排列的强制字段,还有可选扩展字段,即便在强制字段中,X.509证书也容许很大的灵活性,由于它为大多数字段提供了多种编码方案.
JWT 最多见的几种签名算法:HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 还有 ES256(ECDSA-SHA256)。
这三种算法都是一种消息签名算法,获得的都只是一段没法还原的签名。区别在于消息签名与签名验证须要的 「key」不一样。
HS256 使用同一个「secret_key」进行签名与验证。一旦 secret_key 泄漏,就毫无安全性可言了。
RS256 是使用 RSA 私钥进行签名,使用 RSA 公钥进行验证。公钥即便泄漏也毫无影响,只要确保私钥安全就行。
对于单体应用而言,HS256 和 RS256 的安全性没有多大差异。
而对于须要进行多方验证的微服务架构而言,显然 RS256/ES256 安全性更高。
只有 user 微服务须要用 RSA 私钥生成 JWT,其余微服务使用公钥便可进行签名验证,私钥获得了更好的保护。
无状态登陆
微服务集群中的每一个服务, 对外提供的都是Rest风格的接口, 而Rest风格的一个最重要的规范就是: 服务的无状态性, 即:
优势:
jjwt是一个Java对jwt的支持库,咱们使用这个库来建立、解码token
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
配合joda-time处理过时时间
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.6</version> </dependency>
客户端发送 POST 请求到服务器,提交登陆处理的Controller层
调用认证服务进行用户名密码认证,若是认证经过,返回完整的用户信息及对应权限信息
利用 JJWT 对用户、权限信息、秘钥构建Token
返回构建好的Token
下面是关键代码, 文章后面有所有的工具类
/** * 私钥加密生成token * @param user 载荷数据 * @param privateKey 私钥字节数组 * @param expireMinutes 过时时间,单位分钟 * @return */ public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{ return Jwts.builder() .claim(JWTConstants.JWT_KEY_ID, user.getId()) .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName()) .claim(JWTConstants.JWT_KEY_ROLE, user.getRole()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)) .compact(); }
Jwts.builder() 返回了一个 DefaultJwtBuilder()
DefaultJwtBuilder属性
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private Header header; //头部 private Claims claims; //声明 private String payload; //载荷 private SignatureAlgorithm algorithm; //签名算法 private Key key; //签名key private byte[] keyBytes; //签名key的字节数组 private CompressionCodec compressionCodec; //压缩算法
DefaultJwtBuilder包含了一些Header和Payload的一些经常使用设置方法
使用私钥加密的jwt, 公钥和私钥均可以解密
使用公钥加密的jwt, 只有私钥能够解密
客户端向服务器请求,服务端读取请求头信息(request.header)获取Token
若是找到Token信息,则根据配置文件中的签名加密秘钥,调用JJWT Lib对Token信息进行解密和解码;
完成解码并验证签名经过后,对Token中的exp、nbf、aud等信息进行验证;
所有经过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
若是权限逻辑判断经过则经过Response对象返回;不然则返回HTTP 401;
/** * 公钥解析token * @param token 用户请求中的token * @param publicKey 公钥字节数组 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception { return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)) .parseClaimsJws(token); }
Jwts.parser() 返回了DefaultJwtParser 对象
DefaultJwtParser() 属性
//don't need millis since JWT date fields are only second granularity: private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final int MILLISECONDS_PER_SECOND = 1000; private ObjectMapper objectMapper = new ObjectMapper(); private byte[] keyBytes; //签名key字节数组 private Key key; //签名key private SigningKeyResolver signingKeyResolver; //签名Key解析器 private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); //压缩解析器 Claims expectedClaims = new DefaultClaims(); //指望Claims private Clock clock = DefaultClock.INSTANCE; //时间工具实例 private long allowedClockSkewMillis = 0; //容许的时间偏移量
parse() 方法传入一个JWT字符串,返回一个JWT对象
解析过程:
载荷解析: 先对载荷进行Base64解码,若是有通过压缩,那么在解码后再进行解压缩。此时将值赋予payload。若是载荷是json形式,将json键值读入map,将值赋予claims 。
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: Map<String, Object> claimsMap = readValue(payload); claims = new DefaultClaims(claimsMap); }
签名解析: 若是存在签名部分,则对签名进行解析。
可能的异常
建立签名校验器
JJWT实现了一个默认的签名校验器DefaultJwtSignatureValidator。该类提供了两个构造方法,外部调用的构造方法传入算法和签名key,再加上一个DefaultSignatureValidatorFactory工厂实例传递调用另外一个构造函数,以便工厂根据不一样算法建立不一样类型的Validator。
public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); } public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { Assert.notNull(factory, "SignerFactory argument cannot be null."); this.signatureValidator = factory.createSignatureValidator(alg, key); }
import com.uni.entity.ShopUser; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.joda.time.*; import java.security.PrivateKey; import java.security.PublicKey; /** * JWT 的工具类:包含了建立和解码的工具 */ @Slf4j public class JWTUtils { /** * 私钥加密token * @param user 载荷数据 * @param privateKey 私钥 * @param expireMinutes 过时时间,单位分钟 * @return */ public static String generateToken(ShopUser user, PrivateKey privateKey, Integer expireMinutes) throws Exception{ return Jwts.builder() .claim(JWTConstants.JWT_KEY_ID, user.getId()) .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName()) .claim(JWTConstants.JWT_KEY_ROLE, user.getRole()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, privateKey) .compact(); } /** * 私钥加密token * @param user 载荷数据 * @param privateKey 私钥字节数组 * @param expireMinutes 过时时间,单位分钟 * @return */ public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{ return Jwts.builder() .claim(JWTConstants.JWT_KEY_ID, user.getId()) .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName()) .claim(JWTConstants.JWT_KEY_ROLE, user.getRole()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)) .compact(); } /** * 使用公钥解析token * @param token 用户请求中的token * @param publicKey 公钥对象 * @return */ public static Jws<Claims> parserToken(String token, PublicKey publicKey){ return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); } /** * 公钥解析token * @param token 用户请求中的token * @param publicKey 公钥字节数组 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception { return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)) .parseClaimsJws(token); } /** * 获取token中的用户信息 * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 * @throws Exception */ public static ShopUser getInfoFromToken(String token, PublicKey publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID); String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME); Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE); return new ShopUser(user_id, user_name, user_role); } /** * 获取token中的用户信息 * @param token 用户请求中的token * @param publicKey 公钥字节数组 * @return 用户信息 * @throws Exception */ public static ShopUser getInfoFromToken(String token, byte[] publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID); String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME); Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE); return new ShopUser(user_id, user_name, user_role); } /* 测试解析token */ public static void main(String[] args) throws Exception { PublicKey publicKey = RsaUtils.getPublicKey("D://rsa//rsa.pub"); Jws<Claims> claimsJws = parserToken("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0.FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg", publicKey); System.out.println(claimsJws.getSignature()); System.out.println(claimsJws.toString()); } }
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; /** * rsa非对称加密 * 私钥加密,解密须要公钥 */ public class RsaUtils { /** * 从文件中读取公钥 * @param filename 公钥保存路径,相对于classpath * @return 公钥对象 * @throws Exception */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /** * 获取公钥 * X.509是定义了公钥证书结构的基本标准 * @param bytes 公钥的字节形式 * @return 公钥对象 * @throws Exception */ public static PublicKey getPublicKey(byte[] bytes) throws Exception { X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } /** * 从文件中读取私钥 * @param filename 私钥保存路径,相对于classpath * @return 私钥对象 * @throws Exception */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * 获取私钥 * @param bytes 私钥的字节形式 * @return 私钥对象 * @throws Exception */ public static PrivateKey getPrivateKey(byte[] bytes) throws Exception { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(spec); } /** * 根据密文,生成rsa公钥和私钥,并写入指定文件 * @param publicKeyFilename 公钥文件路径 * @param privateKeyFilename 私钥文件路径 * @param secret 生成密钥的密文 * @throws Exception */ public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(secret.getBytes()); keyPairGenerator.initialize(1024, secureRandom); KeyPair keyPair = keyPairGenerator.genKeyPair(); //获取公钥并写出 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); writeFile(publicKeyFilename, publicKeyBytes); //获取私钥并写出 byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); writeFile(privateKeyFilename, privateKeyBytes); } private static byte[] readFile(String filename) throws Exception { return Files.readAllBytes(new File(filename).toPath()); } private static void writeFile(String destPath, byte[] bytes) throws IOException{ File dest = new File(destPath); if (!dest.exists()){ dest.createNewFile(); } Files.write(dest.toPath(), bytes); } /* 测试公私钥获取 */ public static void main(String[] args) throws Exception { //公私钥路径 String pubKeyPath = "D:\\rsa\\rsa.pub"; String priKeyPath = "D:\\rsa\\rsa.pri"; //明文 String secret = "sc@Login(Auth}*^31)&czxy%"; //RsaUtils.generateKey(pubKeyPath, priKeyPath, secret); /* 解密 */ PublicKey publicKey = RsaUtils.getPublicKey(pubKeyPath); System.out.println("公钥: " + publicKey); PrivateKey privateKey = RsaUtils.getPrivateKey(priKeyPath); System.out.println("私钥: " + privateKey); //签名验证器 DefaultJwtSignatureValidator validator = new DefaultJwtSignatureValidator(SignatureAlgorithm.RS256, publicKey); boolean valid = validator.isValid("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0", "FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg"); System.out.println(valid); } }
package com.uni.config; import com.uni.util.RsaUtils; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import javax.annotation.PostConstruct; import java.io.File; import java.security.PrivateKey; import java.security.PublicKey; /** * 初始化公钥和私钥 */ @Slf4j @Data @PropertySource("classpath:application.yml") @ConfigurationProperties(prefix = "jwt") @Configuration public class JWTProperties { private String secret; // 密文 private String pubKeyPath;// 公钥 private String priKeyPath;// 私钥 private Integer expire;// token过时时间 private String[] skipAuthUrls; //跳过的url private PublicKey publicKey; // 公钥 private PrivateKey privateKey; // 私钥 //被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,而且只会被服务器调用一次 @PostConstruct public void init() { try { log.info("公钥地址: " + pubKeyPath); log.info("私钥地址: " + priKeyPath); File pubKey = new File(pubKeyPath); File priKey = new File(priKeyPath); if (!pubKey.exists() || !priKey.exists()) { // 生成公钥和私钥并写入文件 RsaUtils.generateKey(pubKeyPath, priKeyPath, secret); } // 获取公钥和私钥 this.publicKey = RsaUtils.getPublicKey(pubKeyPath); this.privateKey = RsaUtils.getPrivateKey(priKeyPath); } catch (Exception e) { log.error("初始化公钥和私钥失败! " + e); throw new RuntimeException(); } } }
配置以下:
jwt: secret: sc@Login(Auth}*^31)&czxy% # 登陆校验的明文 pubKeyPath: D://rsa//rsa.pub # 公钥地址 priKeyPath: D://rsa//rsa.pri # 私 钥地址 expire: 30 # 过时时间,单位分钟 skipAuthUrls: - /auth/** - ...
public class JWTConstants { public static final String JWT_HEADER_KEY = "Authorization"; public static final String JWT_KEY_ID = "user_id"; public static final String JWT_KEY_USER_NAME = "user_name"; public static final String JWT_KEY_ROLE = "user_role"; }
@Getter @Setter @AllArgsConstructor @NoArgsConstructor public class JWTModel { private Long userId; private String userName; private String jwt; }
import com.uni.config.JWTProperties; import com.uni.entity.Dto; import com.uni.entity.ShopUser; import com.uni.service.ShopUserService; import com.uni.util.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @Slf4j @RestController @RequestMapping("/auth") public class AuthAPI { @Autowired private ShopUserService shopUserService; @Autowired private JWTProperties jwtProperties; @PostMapping("/login") public Dto doLogin(@RequestBody ShopUser user){ ShopUser result = null; // 验证用户明和密码 if (ObjectUtils.isNotEmpty(user)) { result = shopUserService.login(user); } if (ObjectUtils.isEmpty(result)){ return DtoUtil.returnFail("用户名或密码错误", "401"); } try { //生成token String token = JWTUtils.generateToken( result, jwtProperties.getPrivateKey(), 30); return DtoUtil.returnSuccess("登陆成功", new JWTModel(result.getId(), result.getUserName(), token)); } catch (Exception e) { log.error("生成token失败! ", e); return DtoUtil.returnFail("登陆失败", "500"); } } }
import com.uni.config.JWTProperties; import com.uni.util.JWTConstants; import com.uni.util.JWTUtils; import com.uni.util.RsaUtils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.Minutes; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.PropertySource; import org.springframework.core.Ordered; /** * 请求鉴权过滤器 */ @Slf4j @Component public class AccessGateWayFilter implements GlobalFilter, Ordered { private ObjectMapper objectMapper; @Autowired private JWTProperties jwtProperties; @Autowired private AntPathMatcher antPathMatcher; //路径匹配器 public AccessGateWayFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String url = exchange.getRequest().getURI().getPath(); //跳过不须要验证的url for (String skip : jwtProperties.getSkipAuthUrls()) { if (antPathMatcher.match(skip, url)) return chain.filter(exchange); } //获取token String token = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_HEADER_KEY); ServerHttpResponse response = exchange.getResponse(); if (StringUtils.isBlank(token)){ //没有token return authError(response, "请登陆"); } else { try { //解析token Jws<Claims> claims = JWTUtils.parserToken(token, jwtProperties.getPublicKey()); DateTime now = DateTime.now(); DateTime exp = new DateTime(claims.getBody().getExpiration()); log.debug(claims.getBody().getExpiration().toString()); /* 根据具体业务 用户信息&权限验证 */ //claims.getBody()获取载荷 //JWTUtils.getInfoFromToken()获取token中的用户信息 if (valid){ //签名验证经过 return chain.filter(exchange); }else { return authError(response, "认证无效"); } } catch (Exception e) { log.error("检查token时异常: " + e); if (e.getMessage().contains("JWT expired")) return authError(response, "认证过时"); else return authError(response, "认证失败"); } } } /** * 认证错误输出 * @param response 响应对象 * @param msg 错误信息 * @return 响应信息 */ private Mono<Void> authError(ServerHttpResponse response, String msg) { response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type","application/json;charset=UTF-8"); Dto returnFail = DtoUtil.returnFail(msg, HttpStatus.UNAUTHORIZED.toString()); String returnStr = ""; try { returnStr = objectMapper.writeValueAsString(returnFail); } catch (JsonProcessingException e) { e.printStackTrace(); } DataBuffer buffer = response.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Flux.just(buffer)); } @Override public int getOrder() { return -999; } }
令牌的刷新要作到用户无感知的效果, 推荐使用前端拦截器刷新令牌的方式
Web应用程序
一个好的模式是在它过时以前刷新令牌。
将令牌过时时间设置为一周,并在每次用户打开Web应用程序并每隔一小时刷新令牌。若是用户超过一周没有打开过应用程序,那他们就须要再次登陆,这是可接受的Web应用程序UX(用户体验)。
要刷新令牌,API须要一个新的端点,它接收一个有效的、没有过时的JWT、并返回与新的到期字段相同的签名的JWT。而后Web应用程序会将令牌存储在某处。
移动/本地应用程序
大多数本地应用程序的登陆有且仅有一次。
这里面的出发点是,刷新令牌永远不会过时,而且能够始终为有效的JWT进行更换。
永远不会过时的令牌的问题是它失去了令牌的意义。譬如,若是你电话丢了,你该怎么办?所以,它须要由用户以某种方式进行识别,应用程序须要提供撤销访问的方法。咱们决定使用设备的名称,例如“maryo的iPad”。而后用户能够去应用程序,并撤销访问“maryo的iPad”。
另外一种方法是撤销特定事件的刷新令牌,其中一个有趣的事件是更改密码。
咱们认为JWT对于这些用例无效,所以咱们使用随机生成的字符串,并将它们存储在咱们这边。
没有办法完美的将jwt失效
jwt 的目的原本就是为了在服务器不存任何的东西, 用加解密 的 cpu 时间来换取之前要保存的空间 , 说白了就是用 cpu 时间换内存空间(这个内存能够是 session, 也多是 redis 这种)
可能的解决方案:
参考:
https://www.jianshu.com/p/6bf...
https://blog.csdn.net/weixin_...
https://blog.csdn.net/github_...