SpringBoot結合JWT+Shiro+Redis實作token無狀态登入授權
一、引言
在微服務中我們一般采用的是無狀态登入,而傳統的session方式,在前後端分離的微服務架構下,如繼續使用則必将要解決跨域sessionId問題、叢集session共享問題等等。這顯然是費力不讨好的,而整合shiro,卻很不恰巧的與我們的期望有所違背,原因:
(1)shiro預設的攔截跳轉都是跳轉url頁面,而前後端分離後,後端并無權幹涉頁面跳轉。
(2)shiro預設使用的登入攔截校驗機制恰恰就是使用的session。
這當然不是我們想要的,是以如需使用shiro,我們就需要對其進行改造,那麼要如何改造呢?我們可以在整合shiro的基礎上自定義登入校驗,繼續整合JWT,或者oauth2.0等,使其成為支援服務端無狀态登入,即token登入。
二、相關說明
2.1. Shiro + JWT實作無狀态鑒權機制
1. 首先post使用者名與密碼到login進行登入,如果成功在請求頭Header傳回一個加密的Authorization,失敗的話直接傳回未登入,以後通路都帶上這個Authorization即可。
2. 鑒權流程主要是要重寫shiro的入口過濾器BasicHttpAuthenticationFilter,在此基礎上進行攔截、token驗證授權等操作
2.2. 關于AccessToken及RefreshToken概念說明
1. AccessToken:用于接口傳輸過程中的使用者授權辨別,用戶端每次請求都需攜帶,出于安全考慮通常有效時長較短。
2. RefreshToken:與AccessToken為共生關系,一般用于重新整理AccessToken,儲存于服務端,用戶端不可見,有效時長較長。
2.3. 關于Redis中儲存RefreshToken資訊(做到JWT的可控性)
1. 登入認證通過後傳回AccessToken資訊(在AccessToken中儲存目前的時間戳和帳号),同時在Redis中設定一條以帳号為Key,Value為目前時間戳(登入時間)的RefreshToken,現在認證時必須AccessToken沒失效以及Redis存在所對應的RefreshToken,且RefreshToken時間戳和AccessToken資訊中時間戳一緻才算認證通過,這樣可以做到JWT的可控性,如果重新登入擷取了新的AccessToken,舊的AccessToken就認證不了,因為Redis中所存放的的RefreshToken時間戳資訊隻會和最新的AccessToken資訊中攜帶的時間戳一緻,這樣每個使用者就隻能使用最新的AccessToken認證。
2. Redis的RefreshToken也可以用來判斷使用者是否線上,如果删除Redis的某個RefreshToken,那這個RefreshToken所對應的AccessToken之後也無法通過認證了,就相當于控制了使用者的登入,可以剔除使用者
2.4. 關于根據RefreshToken自動重新整理AccessToken
1. 本身AccessToken的過期時間為5分鐘,RefreshToken過期時間為30分鐘,當登入後時間過了5分鐘之後,目前AccessToken便會過期失效,再次帶上AccessToken通路JWT會抛出TokenExpiredException異常說明Token過期,開始判斷是否要進行AccessToken重新整理,首先redis查詢RefreshToken是否存在,以及時間戳和過期AccessToken所攜帶的時間戳是否一緻,如果存在且一緻就進行AccessToken重新整理。
2. 重新整理後新的AccessToken過期時間依舊為5分鐘,時間戳為目前最新時間戳,同時也設定RefreshToken中的時間戳為目前最新時間戳,重新整理過期時間重新為30分鐘過期,最終将重新整理的AccessToken存放在Response的Header中的Authorization字段傳回。
3. 同時前端進行擷取替換,下次用新的AccessToken進行通路即可。
三、項目準備配置
項目結構:

pom.xml
該項目要用到的元件有java-jwt、json、shiro-spring、spring-boot-starter-data-redis等。
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
com.ljnt
blog
0.0.1-SNAPSHOT
blog
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
com.auth0
java-jwt
3.9.0
org.json
json
20190722
org.apache.shiro
shiro-spring
1.4.0
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-maven-plugin
application.yml
主要是配置redis,需要用到模闆、資料庫、日志的,請看注釋
server:
port: 8181
#spring:
# thymeleaf:
# mode: HTML5
#
# datasource:
# driver-class-name: com.mysql.jdbc.Driver
# url: jdbc:mysql://localhost:3306/blog?useSSL=false&characterEncoding=utf-8
# username: root
# password:
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: -1
max-wait: 3000ms
timeout: 3000ms
#logging:
# level:
# root: info
# com.ljnt: debug
# file: log/imcoding.log
四、實作頒發token
實作頒發token需要用到JWT和Redis,是以我們需要配置Redis和實作工具類。
4.1. 配置Redis:RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
// 配置連接配接工廠
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(預設使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修飾符範圍,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化輸入的類型,類必須是非final修飾的,final修飾的類,比如String,Integer等會跑出異常
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 設定hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
@Bean
public HashOperations hashOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations listOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations setOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations zSetOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForZSet();
}
}
4.2. 編寫工具類
RedisUtil,這裡主要用到redisTemplate的一些方法,代碼沒有全部給出來,可以根據redisTemplate方法去編寫或者看我的源碼。。
@Component
public class RedisUtil {
@Autowired
private static RedisTemplate redisTemplate;
public RedisUtil(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static boolean expire(String key,long time){
try {
if(time>0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
public static boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//=========代碼太長,省略代碼
}
TokenUtil,主要實作token的簽發、驗證和資料解析。
public class TokenUtil {
//這裡的token屬性配置最好寫在配置檔案中,這裡為了方面直接寫成靜态屬性
public static final long EXPIRE_TIME= 5*60*1000;//token到期時間5分鐘,毫秒為機關
public static final long REFRESH_EXPIRE_TIME=30*60;//RefreshToken到期時間為30分鐘,秒為機關
private static final String TOKEN_SECRET="ljdyaishijin**3nkjnj??"; //密鑰鹽
public static String sign(String account,Long currentTime){
String token=null;
try {
Date expireAt=new Date(currentTime+EXPIRE_TIME);
token = JWT.create()
.withIssuer("auth0")//發行人
.withClaim("account",account)//存放資料
.withClaim("currentTime",currentTime)
.withExpiresAt(expireAt)//過期時間
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (IllegalArgumentException|JWTCreationException je) {
}
return token;
}
public static Boolean verify(String token) throws Exception{
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();//建立token驗證器
DecodedJWT decodedJWT=jwtVerifier.verify(token);
System.out.println("認證通過:");
System.out.println("account: " + decodedJWT.getClaim("account").asString());
System.out.println("過期時間: " + decodedJWT.getExpiresAt());
return true;
}
public static String getAccount(String token){
try{
DecodedJWT decodedJWT=JWT.decode(token);
return decodedJWT.getClaim("account").asString();
}catch (JWTCreationException e){
return null;
}
}
public static Long getCurrentTime(String token){
try{
DecodedJWT decodedJWT=JWT.decode(token);
return decodedJWT.getClaim("currentTime").asLong();
}catch (JWTCreationException e){
return null;
}
}
}
4.3. 編寫登入接口:LoginController
登入成功頒發token,生成RefreshToken儲存在redis,傳回在Header的Authorization中。
@Controller
public class LoginController {
@Autowired
RedisUtil redisUtil;
@PostMapping("/login")
@ResponseBody
public Result login(String username, String password, HttpServletResponse response) throws JsonProcessingException {
User user=new User();
user.setUsername(username);
user.setPassword(password);
//去資料庫拿密碼驗證使用者名密碼,這裡直接驗證
if(username.equals("admin")){
if (!password.equals("admin")){
return new Result(400,"密碼錯誤");
}
}else if (username.equals("user")){
if (!password.equals("user")){
return new Result(400版權聲明:本文來源CSDN,感謝部落客原創文章,遵循 CC 4.0 by-sa 版權協定,轉載請附上原文出處連結和本聲明。
原文連結:https://blog.csdn.net/ljlj8888/article/details/104826421
站方申明:本站部分内容來自社群使用者分享,若涉及侵權,請聯系站方删除。