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<>(); /** *附加的属性 */ privateMap additional; /** *jwt签发者 **/ privateStringiss; /** *jwt所面向的用户 **/ privateStringsub; /** *接收jwt的一方 **/ privateStringaud; /** *jwt的过期时间,这个过期时间必须要大于签发时间 **/ privateLocalDateTimeexp; /** *jwt的签发时间 **/ privateLocalDateTimeiat=LocalDateTime.now(); /** *权限集 */ privateSet roles=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(Set roles){ this.roles=roles; returnthis; } publicJwtPayloadBuilderexpDays(intdays){ Assert.isTrue(days>0,"jwtexpireDatemustafternow"); this.exp=this.iat.plusDays(days); returnthis; } publicJwtPayloadBuilderadditional(Map additional){ 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(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。