JWT
JWT(JSON Web Token), 是为了在网络应用环境间传递申明而执行的一种基于JSON的凋谢规范((RFC 7519).该token被设计为紧凑且平安的,特地实用于分布式站点的单点登录(SSO)场景。JWT的申明个别被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够减少一些额定的其它业务逻辑所必须的申明信息,该token也可间接被用于认证,也可被加密。
JWT的组成
- Header(头部) —— base64编码的Json字符串
- Payload(载荷) —— base64编码的Json字符串
- Signature(签名)—— 应用指定算法,通过Header和Payload加盐计算的字符串
header
jwt的头部承载两局部信息:
{ 'typ': 'JWT', //申明类型 'alg': 'RS256' //签名加密的算法}
而后将头部进行base64加密(该加密是能够对称解密的),形成了第一局部.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
载荷就是寄存无效信息的中央。这个名字像是特指飞机上承载的货品,这些无效信息蕴含三个局部:
- 规范中注册的申明 (==倡议但不强制应用==) :
{ "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,从而回避重放攻打"}
- ==公共==的申明 :
公共的申明能够增加任何的信息,个别增加用户的相干信息或其余业务须要的必要信息.但不倡议增加敏感信息,因为该局部在客户端可解密. - ==公有==的申明 :
公有申明是提供者和消费者所独特定义的申明,个别不倡议寄存敏感信息,因为base64是对称解密的,意味着该局部信息能够归类为明文信息。
定义一个payload:
// 包含须要传递的用户信息;{ "iss": "Online JWT Builder", "iat": 1416797419, "exp": 1448333419, "aud": "www.gusibi.com", "sub": "uid", "nickname": "goodspeed", "username": "goodspeed", "scopes": [ "admin", "user" ] }
而后将其进行base64加密,失去Jwt的第二局部。
eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVk
signature
jwt的第三局部是一个签证信息,这个签证信息由三局部组成:
// 依据头部alg算法与公有秘钥进行加密失去的签名字符串;// 这一段是最重要的敏感信息,只能在服务端解密;HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECREATE_KEY)
这个局部须要base64加密后的header和base64加密后的payload应用 "." 连贯组成的字符串,而后通过header中申明的加密形式进行加盐secret组合加密,而后就形成了jwt的第三局部。
// javascriptvar 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 透露,就毫无安全性可言了。
- 因而 HS256 只适宜集中式认证,签名和验证都必须由可信方进行。
RS256 是应用 RSA 私钥进行签名,应用 RSA 公钥进行验证。公钥即便透露也毫无影响,只有确保私钥平安就行。
- RS256 能够将验证委托给其余利用,只有将公钥给他们就行。
- ES256 和 RS256 一样,都应用私钥签名,公钥验证。算法速度上差距也不大,然而它的签名长度绝对短很多(省流量),并且算法强度和 RS256 差不多。
对于单体利用而言,HS256 和 RS256 的安全性没有多大差异。
而对于须要进行多方验证的微服务架构而言,显然 RS256/ES256 安全性更高。
只有 user 微服务须要用 RSA 私钥生成 JWT,其余微服务应用公钥即可进行签名验证,私钥失去了更好的爱护。
无状态登录
微服务集群中的每个服务, 对外提供的都是Rest格调的接口, 而Rest格调的一个最重要的标准就是: 服务的无状态性, 即:
- 服务端不保留任何客户端请求者状态信息
- 客户端的每次申请必须具备自描述信息, 通过这些信息辨认客户端身份
长处:
- 客户端申请不依赖服务端的信息, 任何屡次申请不须要必须拜访到同一台服务
- 服务端的集群和状态对客户端通明
- 服务端能够任意的迁徙和伸缩
- 减小服务端存储压力
JJWT
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>
生成JWT
客户端发送 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, 公钥和私钥都能够解密
应用公钥加密的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; //签名keyprivate SigningKeyResolver signingKeyResolver; //签名Key解析器private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); //压缩解析器Claims expectedClaims = new DefaultClaims(); //冀望Claimsprivate Clock clock = DefaultClock.INSTANCE; //工夫工具实例private long allowedClockSkewMillis = 0; //容许的工夫偏移量
parse() 办法传入一个JWT字符串,返回一个JWT对象
解析过程:
- 查看: 以分隔符" . "切分JWT的三个局部。如果分隔符数量谬误或者载荷为空,将抛出 MalformedJwtException 异样。
- 头部解析: 将头部原始Json键值存入map。依据是否加密创立不同的头部对象。jjwt的DefaultCompressionCodecResolver依据头部信息的压缩算法信息,增加不同的压缩解码器。
载荷解析: 先对载荷进行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);}
签名解析: 如果存在签名局部,则对签名进行解析。
- 首先依据头部的签名算法信息,获取对应的算法。
如果签名局部不为空,然而签名算法为null或者'none',将抛出MalformedJwtException异样。 - 获取签名key
可能的异样
- 如果同时设置了key属性和keyBytes属性,parser不晓得该应用哪个值去作为签名key解析,将抛出异样。
- 如果key属性和keyBytes属性只存在一个,然而设置了signingKeyResolver,也不晓得该去解析前者还是应用后者,将抛出异样。
- 如果设置了key(setSigningKey() 办法)则间接应用生成Key对象。如果两种模式( key和keyBytes )都没有设置,则应用SigningKeyResolver(通过setSigningKeyResolver()办法设置)获取key, 当然,获取key为null会抛出异样
创立签名校验器
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);}
- 比对验证
依据头部和载荷从新计算签名并比对。
如果不匹配,抛出SignatureException异样 - 工夫校验
依据以后工夫和工夫偏移判断是否过期。
依据以后工夫和工夫偏移判断是够未到可接管工夫 - Claims参数校验
即校验parser后面设置的所以require局部。校验实现后,以header,claims或者payload创立DefaultJwt对象返回 - 至此,曾经实现JWT Token的校验过程。校验通过后返回JWT对象。
- 首先依据头部的签名算法信息,获取对应的算法。
工具类
JWTUtils
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 的工具类:蕴含了创立和解码的工具 */@Slf4jpublic 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()); }}
RsaUtils
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); }}
JWTProperties
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")@Configurationpublic 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/** - ...
JWTConstants
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";}
JWTModel
@Getter@Setter@AllArgsConstructor@NoArgsConstructorpublic 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@Componentpublic 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; }}
刷新JWT
令牌的刷新要做到用户无感知的成果, 举荐应用前端拦截器刷新令牌的形式
Web应用程序
一个好的模式是在它过期之前刷新令牌。
将令牌过期工夫设置为一周,并在每次用户关上Web应用程序并每隔一小时刷新令牌。如果用户超过一周没有关上过应用程序,那他们就须要再次登录,这是可承受的Web应用程序UX(用户体验)。
要刷新令牌,API须要一个新的端点,它接管一个无效的、没有过期的JWT、并返回与新的到期字段雷同的签名的JWT。而后Web应用程序会将令牌存储在某处。
挪动/本地应用程序
大多数本地应用程序的登录有且仅有一次。
这外面的出发点是,刷新令牌永远不会过期,并且能够始终为无效的JWT进行更换。
永远不会过期的令牌的问题是它失去了令牌的意义。譬如,如果你电话丢了,你该怎么办?因而,它须要由用户以某种形式进行辨认,应用程序须要提供撤销拜访的办法。咱们决定应用设施的名称,例如“maryo的iPad”。而后用户能够去应用程序,并撤销拜访“maryo的iPad”。
另一种办法是撤销特定事件的刷新令牌,其中一个乏味的事件是更改明码。
咱们认为JWT对于这些用例有效,因而咱们应用随机生成的字符串,并将它们存储在咱们这边。
登记
没有方法完满的将jwt生效
jwt 的目标原本就是为了在服务器不存任何的货色, 用加解密 的 cpu 工夫来换取以前要保留的空间 , 说白了就是用 cpu 工夫换内存空间(这个内存能够是 session, 也可能是 redis 这种)
可能的解决方案:
- 将JWT存储在数据库中。您能够查看哪些令牌无效以及哪些令牌已被撤销,但这在我看来齐全违反了应用JWT的目标。
- 从客户端删除令牌。这将阻止客户端进行通过身份验证的申请,但如果令牌依然无效且其他人能够拜访它,则仍能够应用该令牌。这引出了我的下一点。
- 令牌生命周期短。让令牌疾速到期。依据利用,可能是几分钟或半小时。当客户端删除其令牌时,会有一个很短的工夫窗口依然能够应用它。从客户端删除令牌并具备短令牌生存期不须要对后端进行重大批改。然而令牌生命周期短意味着用户因令牌已过期而一直被登记。
- 旋转代币。兴许引入刷新令牌的概念。当用户登录时,为他们提供JWT和刷新令牌。将刷新令牌存储在数据库中。对于通过身份验证的申请,客户端能够应用JWT,然而当令牌过期(或行将过期)时,让客户端应用刷新令牌发出请求以换取新的JWT。这样,您只需在用户登录或要求新的JWT时拜访数据库。当用户登记时,您须要使存储的刷新令牌有效。否则,即便用户曾经登记,有人在监听连贯时依然能够取得新的JWT。
- 创立JWT黑名单。依据过期工夫,当客户端删除其令牌时,它可能依然无效一段时间。如果令牌生存期很短,则可能不是问题,但如果您仍心愿令牌立刻生效,则能够创立令牌黑名单。当后端收到登记申请时,从申请中获取JWT并将其存储在内存数据库中。对于每个通过身份验证的申请,您须要查看内存数据库以查看令牌是否已生效。为了放弃较小的搜寻空间,您能够从黑名单中删除曾经过期的令牌。
参考:
https://www.jianshu.com/p/6bf...
https://blog.csdn.net/weixin_...
https://blog.csdn.net/github_...