简单描述:最近在处理鉴权这一块的东西,需求就是用户登录需要获取token,然后携带token访问接口,token认证成功接口才能返回正确的数据,如果访问接口时候token过期,就采用刷新token刷新令牌(得到新的token和refresh_token),然后在访问接口返回数据,如果刷新token也过期了,就提示用户重新登录。废话不多说,直接上代码。源码在github上
使用 springboot + thymeleaf + mybatis 搭建的
//核心依赖 <!-- spring security + OAuth2 + JWT start -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security + OAuth2 + JWT end -->
//核心配置 application.yml文件
#toekn相关配置
token:
config:
#客户端标识,类比为token的用户名,我写的是项目名
clientId: SOJ_DEMO
#客户端安全码,类比为token的密码,我写的是假邮箱
secret: soj@123
#表示授权模式: password(密码模式),authorization_code(授权码模式)
grantTypes: password
#表示权限范围,该属性为可选项
scopes: all
#令牌的有效时长,此处为180s/60,时长为2分钟 设置短一点是为了测试刷新token
accessTokenValidity: 120
#刷新令牌的有效时长
refreshTokenValidity: 36000
#资源ID号
resourceId: SOJ_DEMO
#token签名的key,用于token对称加解密
signingKey: SOJ_SYMMETRY
#设置刷新令牌机制.true(重复使用:更新access_token时长后,refresh_toke时长不更新)。false(与true相反)
isRefreshToken: false
利用keytool工具生成密钥对(非对称秘钥 公钥私钥) 来执行签名过程 keytool工具使用帮助文档 这里得好好看看 最好一步步来 繁琐的过程,我的妈呀 真的要吐了 太恶心了
//在cmd命令提示符中执行此命令 绿色部分是变量 你可以修改
keytool -genkeypair -alias jwt -keyalg RSA -keypass xc1234 -keystore jwt.jks -storepass xc1234
keytool -genkeypair -alias jwt -keyalg RSA -keypass xc1234 -keystore jwt.jks -storepass xc1234
执行完之后会在C:\Users\Administrator目录下生成一个jwt.jks文件, .jks文件包含了我们的秘钥 公钥 私钥,后续会在代码中读取此文件中的公钥私钥。
把生成的jwt.jks文件放到resources目录下的certificate文件夹中,这里给一下目录结构
然后在POM文件中加入对此文件的引入
//pom文件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>certificate/*.jks</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>certificate/*.jks</include>
</includes>
</resource>
</resources>
</build>
下边开始代码
JwtToken主要负责token解析和加解密 和上边的jwt.jks文件打交道 读取公钥私钥
package com.xc.soj_demo.jwt;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
/**
* token加解密
*
* 1.对称加解密
* 2.非对称加解密(RSA)
*
*/
@Configuration
public class JwtToken {
/**
* 加载jwt.jks文件
*/
private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("certificate/jwt.jks");
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;
static {
try {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, "xc1234".toCharArray());
privateKey = (PrivateKey) keyStore.getKey("jwt", "xc1234".toCharArray());
publicKey = keyStore.getCertificate("jwt").getPublicKey();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成jwt token(非对称加密模式 公钥私钥)
*/
public static String generateTokenRSA(String subject, int expirationSeconds) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
return Jwts.builder()
.setClaims(null)
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 解析jwt token(非对称加密模式 公钥私钥)
*/
public static Claims parseTokenRSA(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
try {
return Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
System.out.println("try-catch:validate is token error ");
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public static boolean isTokenExpired(Date expiration) {
boolean before = expiration.before(new Date());
return before;
}
/**
* 生成jwt token(对称加密模式)
*/
public static String generateToken(String subject, int expirationSeconds,
String signingKey) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expirationSeconds * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, signingKey)
.compact();
}
/**
* 解析jwt token(对称加密模式)
*/
public static String parseToken(String token,String signingKey) {
if (StringUtils.isEmpty(token)) {
return null;
}
token = StringUtils.substringAfter(token, "bearer");//定义token令牌的类型为bearer
Claims claims;
try {
claims = Jwts.parser().setSigningKey(signingKey.getBytes("UTF-8")).parseClaimsJws(token).getBody();
} catch (ClaimJwtException e) {
//源码DefaultJwtParser.Class中的处理过程是 从token中取出载荷payload部分,解析出claim(claim中存在用户信息),然后在解析是否过期,最后才抛出的异常
//所以是可以从 ClaimJwtException e中取出需要的部分 并且源码ClaimJwtException.Class类中有header和claim两个私有属性并提供了get方法
claims = e.getClaims();
} catch (UnsupportedEncodingException e) {
return null;
}
String localUser = (String) claims.get("userinfo");// 拿到当前用户
return localUser;
}
}