JWT Token实现方法及步骤详解
1.前言
JsonWebToken(JWT)近几年是前后端分离常用的Token技术,是目前最流行的跨域身份验证解决方案。你可以通过文章一文了解web无状态会话token技术JWT来了解JWT。今天我们来手写一个通用的JWT服务。DEMO获取方式在文末,实现在jwt相关包下
2.spring-security-jwt
spring-security-jwt是SpringSecurityCrypto提供的JWT工具包。
org.springframework.security spring-security-jwt ${spring-security-jwt.version}
核心类只有一个:org.springframework.security.jwt.JwtHelper。它提供了两个非常有用的静态方法。
3.JWT编码
JwtHelper提供的第一个静态方法就是encode(CharSequencecontent,Signersigner)这个是用来生成jwt的方法需要指定payload跟signer签名算法。payload存放了一些可用的不敏感信息:
- issjwt签发者
- subjwt所面向的用户
- aud接收jwt的一方
- iatjwt的签发时间
- expjwt的过期时间,这个过期时间必须要大于签发时间iat
- jtijwt的唯一身份标识,主要用来作为一次性token,从而回避重放***
除了以上提供的基本信息外,我们可以定义一些我们需要传递的信息,比如目标用户的权限集等等。切记不要传递密码等敏感信息,因为JWT的前两段都是用了BASE64编码,几乎算是明文了。
3.1构建JWT中的payload
我们先来构建payload:
/**
*构建jwtpayload
*
*@authorFelordcn
*@since11:272019/10/25
**/
publicclassJwtPayloadBuilder{
privateMappayload=newHashMap<>();
/**
*附加的属性
*/
privateMapadditional;
/**
*jwt签发者
**/
privateStringiss;
/**
*jwt所面向的用户
**/
privateStringsub;
/**
*接收jwt的一方
**/
privateStringaud;
/**
*jwt的过期时间,这个过期时间必须要大于签发时间
**/
privateLocalDateTimeexp;
/**
*jwt的签发时间
**/
privateLocalDateTimeiat=LocalDateTime.now();
/**
*权限集
*/
privateSetroles=newHashSet<>();
/**
*jwt的唯一身份标识,主要用来作为一次性token,从而回避重放***
**/
privateStringjti=IdUtil.simpleUUID();
publicJwtPayloadBuilderiss(Stringiss){
this.iss=iss;
returnthis;
}
publicJwtPayloadBuildersub(Stringsub){
this.sub=sub;
returnthis;
}
publicJwtPayloadBuilderaud(Stringaud){
this.aud=aud;
returnthis;
}
publicJwtPayloadBuilderroles(Setroles){
this.roles=roles;
returnthis;
}
publicJwtPayloadBuilderexpDays(intdays){
Assert.isTrue(days>0,"jwtexpireDatemustafternow");
this.exp=this.iat.plusDays(days);
returnthis;
}
publicJwtPayloadBuilderadditional(Mapadditional){
this.additional=additional;
returnthis;
}
publicStringbuilder(){
payload.put("iss",this.iss);
payload.put("sub",this.sub);
payload.put("aud",this.aud);
payload.put("exp",this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ss")));
payload.put("iat",this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ss")));
payload.put("jti",this.jti);
if(!CollectionUtils.isEmpty(additional)){
payload.putAll(additional);
}
payload.put("roles",JSONUtil.toJsonStr(this.roles));
returnJSONUtil.toJsonStr(JSONUtil.parse(payload));
}
}
通过建造类JwtClaimsBuilder我们可以很方便来构建JWT所需要的payloadjson字符串传递给encode(CharSequencecontent,Signersigner)中的content。
3.2生成RSA密钥并进行签名
为了生成JWTToken我们还需要使用RSA算法来进行签名。这里我们使用JDK提供的证书管理工具Keytool来生成RSA证书,格式为jks格式。
生成证书命令参考:
```shellscriptkeytool-genkey-aliasfelordcn-keypassfelordcn-keyalgRSA-storetypePKCS12-keysize1024-validity365-keystored:/keystores/felordcn.jks-storepass123456-dname"CN=(Felord),OU=(felordcn),O=(felordcn),L=(zz),ST=(hn),C=(cn)"
其中`-aliasfelordcn-storepass123456`我们要作为配置使用要记下来。我们要使用下面定义的这个类来读取证书
```java
packagecn.felord.spring.security.jwt;
importorg.springframework.core.io.ClassPathResource;
importjava.security.KeyFactory;
importjava.security.KeyPair;
importjava.security.KeyStore;
importjava.security.PublicKey;
importjava.security.interfaces.RSAPrivateCrtKey;
importjava.security.spec.RSAPublicKeySpec;
/**
*KeyPairFactory
*
*@authorFelordcn
*@since13:412019/10/25
**/
classKeyPairFactory{
privateKeyStorestore;
privatefinalObjectlock=newObject();
/**
*获取公私钥.
*
*@paramkeyPathjks文件在resources下的classpath
*@paramkeyAliaskeytool生成的-alias值felordcn
*@paramkeyPasskeytool生成的-keypass值felordcn
*@returnthekeypair公私钥对
*/
KeyPaircreate(StringkeyPath,StringkeyAlias,StringkeyPass){
ClassPathResourceresource=newClassPathResource(keyPath);
char[]pem=keyPass.toCharArray();
try{
synchronized(lock){
if(store==null){
synchronized(lock){
store=KeyStore.getInstance("jks");
store.load(resource.getInputStream(),pem);
}
}
}
RSAPrivateCrtKeykey=(RSAPrivateCrtKey)store.getKey(keyAlias,pem);
RSAPublicKeySpecspec=newRSAPublicKeySpec(key.getModulus(),key.getPublicExponent());
PublicKeypublicKey=KeyFactory.getInstance("RSA").generatePublic(spec);
returnnewKeyPair(publicKey,key);
}catch(Exceptione){
thrownewIllegalStateException("Cannotloadkeysfromstore:"+resource,e);
}
}
}
获取了KeyPair就能获取公私钥生成Jwt的两个要素就完成了。我们可以和之前定义的JwtPayloadBuilder一起封装出生成JwtToken的方法:
privateStringjwtToken(Stringaud,intexp,Setroles,Map additional){ Stringpayload=jwtPayloadBuilder .iss(jwtProperties.getIss()) .sub(jwtProperties.getSub()) .aud(aud) .additional(additional) .roles(roles) .expDays(exp) .builder(); RSAPrivateKeyprivateKey=(RSAPrivateKey)keyPair.getPrivate(); RsaSignersigner=newRsaSigner(privateKey); returnJwtHelper.encode(payload,signer).getEncoded(); }
通常情况下JwtToken都是成对出现的,一个为平常请求携带的accessToken,另一个只作为刷新accessToken之用的refreshToken。而且refreshToken的过期时间要相对长一些。当accessToken失效而refreshToken有效时,我们可以通过refreshToken来获取新的JwtToken对;当两个都失效就用户就必须重新登录了。
生成JwtToken对的方法如下:
publicJwtTokenPairjwtTokenPair(Stringaud,Setroles,Map additional){ StringaccessToken=jwtToken(aud,jwtProperties.getAccessExpDays(),roles,additional); StringrefreshToken=jwtToken(aud,jwtProperties.getRefreshExpDays(),roles,additional); JwtTokenPairjwtTokenPair=newJwtTokenPair(); jwtTokenPair.setAccessToken(accessToken); jwtTokenPair.setRefreshToken(refreshToken); //放入缓存 jwtTokenStorage.put(jwtTokenPair,aud); returnjwtTokenPair; }
通常JwtToken对会在返回给前台的同时放入缓存中。过期策略你可以选择分开处理,也可以选择以refreshToken的过期时间为准。
4.JWT解码以及验证
JwtHelper提供的第二个静态方法是JwtdecodeAndVerify(Stringtoken,SignatureVerifierverifier)用来验证和解码JwtToken。我们获取到请求中的token后会解析出用户的一些信息。通过这些信息去缓存中对应的token,然后比对并验证是否有效(包括是否过期)。
/**
*解码并校验签名过期不予解析
*
*@paramjwtTokenthejwttoken
*@returnthejwtclaims
*/
publicJSONObjectdecodeAndVerify(StringjwtToken){
Assert.hasText(jwtToken,"jwttokenmustnotbebank");
RSAPublicKeyrsaPublicKey=(RSAPublicKey)this.keyPair.getPublic();
SignatureVerifierrsaVerifier=newRsaVerifier(rsaPublicKey);
Jwtjwt=JwtHelper.decodeAndVerify(jwtToken,rsaVerifier);
Stringclaims=jwt.getClaims();
JSONObjectjsonObject=JSONUtil.parseObj(claims);
Stringexp=jsonObject.getStr(JWT_EXP_KEY);
//是否过期
if(isExpired(exp)){
thrownewIllegalStateException("jwttokenisexpired");
}
returnjsonObject;
}
上面我们将有效的JwtToken中的payload解析为JSON对象,方便后续的操作。
5.配置
我们将JWT的可配置项抽出来放入JwtProperties如下:
/**
*Jwt在springbootapplication.yml中的配置文件
*
*@authorFelordcn
*@since15:062019/10/25
*/
@Data
@ConfigurationProperties(prefix=JWT_PREFIX)
publicclassJwtProperties{
staticfinalStringJWT_PREFIX="jwt.config";
/**
*是否可用
*/
privatebooleanenabled;
/**
*jks路径
*/
privateStringkeyLocation;
/**
*keyalias
*/
privateStringkeyAlias;
/**
*keystorepass
*/
privateStringkeyPass;
/**
*jwt签发者
**/
privateStringiss;
/**
*jwt所面向的用户
**/
privateStringsub;
/**
*accessjwttoken有效天数
*/
privateintaccessExpDays;
/**
*refreshjwttoken有效天数
*/
privateintrefreshExpDays;
}
然后我们就可以配置JWT的javaConfig如下:
/**
*JwtConfiguration
*
*@authorFelordcn
*@since16:542019/10/25
*/
@EnableConfigurationProperties(JwtProperties.class)
@ConditionalOnProperty(prefix="jwt.config",name="enabled")
@Configuration
publicclassJwtConfiguration{
/**
*Jwttokenstorage.
*
*@returnthejwttokenstorage
*/
@Bean
publicJwtTokenStoragejwtTokenStorage(){
returnnewJwtTokenCacheStorage();
}
/**
*Jwttokengenerator.
*
*@paramjwtTokenStoragethejwttokenstorage
*@paramjwtPropertiesthejwtproperties
*@returnthejwttokengenerator
*/
@Bean
publicJwtTokenGeneratorjwtTokenGenerator(JwtTokenStoragejwtTokenStorage,JwtPropertiesjwtProperties){
returnnewJwtTokenGenerator(jwtTokenStorage,jwtProperties);
}
}
然后你就可以通过JwtTokenGenerator编码/解码验证JwtToken对,通过JwtTokenStorage来处理JwtToken缓存。缓存这里我用了SpringCacheEhcache来实现,你也可以切换到Redis。相关单元测试参见DEMO
6.总结
今天我们利用spring-security-jwt手写了一套JWT逻辑。无论对你后续结合SpringSecurity还是Shiro都十分有借鉴意义。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。