共计 20469 个字符,预计需要花费 52 分钟才能阅读完成。
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 的第三局部。
// 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 透露,就毫无安全性可言了。
- 因而 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; // 签名 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 对象
解析过程:
- 查看: 以分隔符 ” . “ 切分 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 的工具类:蕴含了创立和解码的工具
*/
@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());
}
}
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")
@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/**
- ...
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
@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;}
}
刷新 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_…