Shiro+Jwt+Redis

如何整合Shiro+Jwt+Redis,以及為什么要這么做

我個人認為


①為什么用shiro:“Shiro+Jwt+Redis”模式和“單純的shiro”模式相比,主要用的是shiro里面的登錄認證和權限控制功能

②為什么用jwt:“Shiro+Jwt”模式和“Shiro+Cookie”模式相比,后者的用戶登錄信息是存儲在服務器的會話里面的,也就是后端服務器的緩存里面,這樣的話就沒辦法分布式(多個后端),解決辦法是把登錄信息以及過期時間直接存儲在一段字符串中,然后由前端保存,后端只需根據生成token時定義的秘鑰去驗證jwt是否正確即可,如果正確就允許接下來的操作。

③為什么用Redis:“Shiro+Jwt+Redis”模式和“Shiro+Jwt”模式相比,前者可以實現分布式環境下的會話共享,這么說有點抽象,通俗一點就是:在分布式系統中,用戶的會話信息需要在多個服務器之間共享,而我可以把用戶的一些前端經常請求的用戶信息或者其他信息存儲到redis里面,這樣就不用去經常查詢數據庫信息了。


所以綜上所述,我們使用Shiro+Jwt+Redis的模式。

Jwt

? 需要了解一門技術,首先從為什么產生開始說起是最好的。JWT 主要用于用戶登錄鑒權,所以我們從最傳統的 session 認證開始說起。

前置知識


**會話:**每個用戶的一次登錄到登出之間叫做一個會話。



登錄狀態:“無狀態”和“有狀態”是指對于服務器而言的兩種不同的處理方式:

  1. 無狀態(Stateless):在無狀態的認證機制中,服務器不需要保存任何關于客戶端的狀態信息。每次客戶端發送請求時,服務器只需要對請求進行處理,而無需考慮之前的請求狀態。這意味著服務器可以更容易地進行水平擴展,因為不需要擔心請求會被路由到特定的服務器上。
  2. 有狀態(Stateful):相比之下,在有狀態的認證機制中,服務器需要保存客戶端的狀態信息,通常通過會話對象或其他方式來記錄客戶端的狀態。這意味著服務器需要在多個請求之間共享狀態信息,可能需要使用特定的機制來保證狀態的一致性和可靠性。

session認證

image-20240519190947325

? 眾所周知,http 協議本身是無狀態的協議(http 是一種無狀態協議,就是說每次用戶進行用戶名和密碼認證之后,http 不會留下記錄,下一次請求還需要進行認證。因為http 不知道每次請求是哪一個用戶發出的)。

? session認證就是說用戶登錄后把將此用戶的登錄狀態存儲到服務器的內存中。

? session 認證的缺點其實很明顯,由于 session 是保存在服務器里,所以如果分布式部署應用的話,會出現session不能共享的問題,很難擴展。

token認證

image-20240519191004738

? token認證的過程就是在用戶第一次登錄的時候根據秘鑰(一般秘鑰中會包括此用戶的唯一標志,比如賬號)生成此次會話的token,然后之后前端每次訪問后端都攜帶token,后端再根據秘鑰解析,如果解析成功就說明token有效,進而可以信任此次請求進行接下來的操作。

? 基于 token 的認證方式是一種服務端無狀態的認證方式,服務端不存儲 token 數據,適合分布式系統。

什么是JWT

? 而JWT(全稱:Json Web Token)是一種特殊的Token,它采用了JSON格式來對Token進行編碼和解碼,并攜帶了更多的信息,例如用戶ID、角色、權限等。它包含了三部分:頭部(Header)、數據(Payload)和簽名(Signature)。其中,頭部和數據都是經過Base64編碼的JSON字符串,而簽名是對頭部和數據進行簽名后得到的字符串。

Springboot使用JWT實現登錄認證以及請求攔截

主要是兩步:配置攔截器、配置要攔截哪些接口

<!--        jwt工具--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency>
package com.hebut.demo.common.utils;import cn.hutool.core.codec.Base64;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;// JWT工具類
@Configuration
public class JwtUtil {@Value("${shiro.jwt.secret}")private static String secret;@Value("${shiro.jwt.expire}")private static Long expire;@Value("${shiro.jwt.header.alg}")private static String headerAlg;@Value("${shiro.jwt.header.typ}")private static String headerTyp;/*** 生成token*/public static String getToken(String account) {// 設置秘鑰StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account).append(secret);// 設置jwt頭headerMap<String, Object> headerClaims = new HashMap<>();headerClaims.put("alg", headerAlg); // 簽名算法headerClaims.put("typ", headerTyp); // token 類型// 設置jwt的header,負載paload以及加密算法String token = JWT.create().withHeader(headerClaims).withClaim("account" ,account).withClaim("expire", System.currentTimeMillis()+expire).sign(Algorithm.HMAC256(stringBuilder.toString()));return token;}/*** 無需秘鑰就能獲取其中的信息* 解析token.* {* "account": "account",* "timeStamp": "134143214"* }*/public static Map<String, String> parseToken(String token) {HashMap<String, String> map = new HashMap<String, String>();// 解碼 JWTDecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");map.put("account", account.asString());map.put("expire", expire.asLong().toString());return map;}/*** 解析token獲取賬號.*/public static String getAccount(String token) {HashMap<String, String> map = new HashMap<String, String>();DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");return account.asString();}/*** 校驗token是否正確* @param token Token* @return boolean 是否正確*/public static boolean verify(String token) {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(getAccount(token)).append(secret);// 帳號加JWT私鑰解密Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());JWTVerifier verifier = JWT.require(algorithm).build();try {verifier.verify(token);return true; // 驗證成功} catch (JWTVerificationException e) {return false; // 驗證失敗}}}
 
import com.demo.util.JWTUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;// 配置攔截器
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");if(StringUtils.isEmpty(token)){throw new Exception("token不能為空");}try {//在這里調用了 JWTUtils工具類的方法 驗證傳入token的合法性,你可以傳token:111 試試JWTUtils.verify(token);} catch (SignatureVerificationException e) {log.error("無效簽名! 錯誤 ->", e);return false;} catch (TokenExpiredException e) {log.error("token過期! 錯誤 ->", e);return false;} catch (AlgorithmMismatchException e) {log.error("token算法不一致! 錯誤 ->", e);return false;} catch (Exception e) {log.error("token無效! 錯誤 ->", e);return false;}return true;}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 配置要攔截哪些接口
@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTInterceptor())//攔截的路徑.addPathPatterns("/**")//排除登錄接口 /test/login 表示你給控制器起的名稱/控制器下的方法,如login.excludePathPatterns("/test/login");}
}

Shiro

? Shiro提供了哪些功能呢?

  1. **登錄認證(Authentication):**Shiro可以對用戶進行身份驗證,確保用戶是合法的。它支持多種認證方式,包括用戶名/密碼、基于證書的認證、第三方登錄等。
  2. **訪問授權(Authorization):**Shiro可以對用戶進行授權,確定用戶是否有權限執行某個操作或訪問某個資源。它支持基于角色的訪問控制和基于權限的訪問控制,可以定義細粒度的權限規則。
  3. **會話管理(Session Management):**Shiro可以管理用戶的會話,包括跟蹤用戶的登錄狀態、管理會話的生命周期、實現單點登錄等功能。
  4. **密碼加密(Password Encryption):**Shiro可以幫助應用程序安全地存儲和驗證用戶密碼,它提供了多種加密算法和技術,如哈希算法、加鹽、散列迭代等。
  5. **RememberMe功能:**Shiro提供了RememberMe功能,可以在用戶登錄后記住用戶的身份,下次訪問時自動登錄。
  6. **Web支持:**Shiro提供了與Web應用程序集成的支持,可以輕松地保護Web資源、處理表單登錄、實現注銷等功能。
  7. **緩存支持:**Shiro支持將重要數據(如用戶信息、權限信息)緩存在內存中,提高系統的性能和響應速度。

? 不用害怕,因為我們就用到了**“登錄認證”和"訪問授權"**。本篇文章主要講解“認證”、“授權”的功能。

主要模塊講解

image-20240521094154037

①Realm用于獲取用戶信息,在這里可以給登錄認證以及訪問授權這兩個事務查詢用戶相關數據,查詢完用戶數據之后,返回一個SimpleAuthorizationInfo類型的對象,交給SecurityManager管理。

②SecurityManager將從Realm得到的信息賦值給對應的subject用于進行登錄認證或者訪問授權

③在一個用戶登錄到退出的整個過程,SecurityManager會一直為此用戶保持一個會話session,會話信息存儲在內存中,以便期間各種訪問。

登錄認證

? shiro提供了方便的登錄認證,可以通過subject.login(token)進行登錄操作。

整體流程

①前端用戶輸入賬號密碼

②后端通過賬號密碼生成Shiro提供的UsernamePasswordToken類型的token

③調用shiro用戶對象的登錄方法subject.login(token)

④subject.login(token)會去調用多個方法,其中兩個是public boolean supports(AuthenticationToken authenticationToken)protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken),前者判斷所傳入的token是不是shiro所支持的token也就是是不是UsernamePasswordToken類型的,后者用于獲取用戶信息。

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法講解,在這個方法里面有兩步,一步是根據token解析出來用戶principal(也就是賬號),然后使用principal去數據庫或者其他數據源拿此用戶對應的唯一憑證credentials(也就是密碼),拿到之后創建SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)對象,傳入三個參數,前兩個是賬號密碼,最后一個是你自定義的realm類的名字。

⑥subject.login(token)就會比對realm返回的用戶賬號密碼是否一致。

⑦除此之外,shiro還提供加密功能,比如用戶的密碼使用了md5加密,那么在配置類里面就可以聲明加密的算法,之后用戶調用subject.login(token)方法的時候就會自動給前端傳給后端的密碼加密,進而直接和realm中獲取的數據進行比對。

代碼實現

需要寫兩個類,一個shiroconfig配置類,一個realm的方法重寫。

UserRealm

// 需要重寫兩個方法
public class UserRealm extends AuthorizingRealm
{/*** 授權:這里先不實現*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){return null;}/*** 登錄認證,自定義登錄認證方式:賬號是否存在,token是否過期* 重寫之后,如果需要登錄認證的接口就會自動調用此接口進行登錄認證*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;String account = usernamePasswordToken.getUsername();// 然后根據賬號從數據庫或者其他數據源查新密碼String password = select(account)// 這一行是偽代碼,select換成自己的查詢邏輯就行return new SimpleAuthenticationInfo(account, password, getName());// getName是AuthorizingRealm實現的方法,將會返回此類的名字,比如在這里就是“UserRealm”}}

ShiroConfig

@Configuration
public class ShiroConfig {// 初始化SecurityManager,把自定義的Realm交給SecurityManager管理@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();UserRealm userRealm = new UserRealm();defaultWebSecurityManager.setRealm(userRealm);return defaultWebSecurityManager;}
}

訪問授權

? 訪問授權就是通過配置shiro的攔截器攔截前端訪問請求,然后再通過Realm獲取用戶的權限信息,如果訪問用戶有此權限就通過攔截器。

整體流程

①設置要攔截哪些路徑或者接口,大概有兩個方法:注解形式攔截器中配置

②用戶攜帶token訪問后端接口

③shiro攔截器攔截,然后調用Realm的protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)方法通過查詢數據庫或者其他數據源獲得此用戶的權限信息,將權限信息添加到SimpleAuthorizationInfo info = new SimpleAuthorizationInfo()實例中并返回。

代碼實現

UserRealm

// 需要重寫兩個方法
public class UserRealm extends AuthorizingRealm
{/*** 授權:這里先不實現*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){return null;}/*** 登錄認證,自定義登錄認證方式:賬號是否存在,token是否過期* 重寫之后,如果需要登錄認證的接口就會自動調用此接口進行登錄認證*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;String account = usernamePasswordToken.getUsername();// 然后根據賬號從數據庫或者其他數據源查新密碼String password = select(account)// 這一行是偽代碼,select換成自己的查詢邏輯就行return new SimpleAuthenticationInfo(account, password, getName());// getName是AuthorizingRealm實現的方法,將會返回此類的名字,比如在這里就是“UserRealm”}}

ShiroConfig

@Configuration
public class ShiroConfig {// 初始化SecurityManager,把自定義的Realm交給SecurityManager管理@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();UserRealm userRealm = new UserRealm();defaultWebSecurityManager.setRealm(userRealm);return defaultWebSecurityManager;}/*** 添加自己的過濾器,自定義url規則,* Filter工廠,設置對應的過濾條件和跳轉條件* Shiro自帶攔截器配置規則* 詳情見文檔 http://shiro.apache.org/web.html#urls-** @date 2018/8/31 10:57*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 添加自己的過濾器并且取名為jwtMap<String, Filter> filterMap = new HashMap<>();filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//        // 登出
//        filterChainDefinitionMap.put("/logout", "logout");// 登錄頁面可以匿名訪問filterChainDefinitionMap.put("/sys/login", "anon");
//        // 首頁需要身份驗證后才能訪問
//        filterChainDefinitionMap.put("/index", "authc");
//        // 錯誤頁面,認證不通過跳轉
//        filterChainDefinitionMap.put("/error", "authc");
//        // 其他頁面需要具有 admin 角色才能訪問
//        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//        // 其他頁面需要具有 user:create 權限才能訪問
//        filterChainDefinitionMap.put("/sys/login", "perms[sys:login]");
//        // 其他頁面需要具有 user:update 和 user:delete 權限才能訪問
//        filterChainDefinitionMap.put("/user/manage", "perms[\"user:update,user:delete\"]");filterChainDefinitionMap.put("/**", "jwt");  // /**,一般放在最下,表示對所有資源起作用,使用JwtFiltershiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*開啟注解的權限控制@RequiresAuthentication(標識用戶必須在當前會話中進行了身份驗證(登錄)才能訪問被注解的方法或類)@RequiresUser(標識用戶必須在應用程序中進行了身份驗證(不一定是當前會話)才能訪問被注解的方法或類)@RequiresGuest(標識用戶必須是一個“guest”(未經身份驗證)才能訪問被注解的方法或類)@RequiresRoles(標識用戶必須具有指定的角色才能訪問被注解的方法或類。可以指定一個或多個角色,如 @RequiresRoles("admin") 或 @RequiresRoles({"admin", "user"}))@RequiresPermissions(標識用戶必須具有指定的權限才能訪問被注解的方法或類。可以指定一個或多個權限,如 @RequiresPermissions("user:create") 或 @RequiresPermissions({"user:create", "user:update"}))@RequiresGuest(標識用戶必須是一個“guest”(未經身份驗證)才能訪問被注解的方法或類)*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}
}

Redis

? 那為什么還要用Redis呢,從Shiro章節可以得知,如果要實現每次訪問后端接口進行登錄認證攔截的話,都要調用Realm中的登錄認證方法,這樣的話每次都要查詢數據庫,數據庫壓力太大,所以我們使用Redis來存儲用戶登錄信息來解決這個問題,除此之外Redis里面還能存儲更多前端經常要訪問到的用戶信息,省的經常去數據庫里面查詢了。

Springboot整合Shiro+Jwt+Redis

數據流向和項目結構

數據流向

image-20240521214547303

項目結構

image-20240521145229144

Redis配置

配置文件,配置redis地址

spring:redis:host: 10.1.40.83port: 6379password:database: 0timeout: 5000lettuce:pool:max-idle: 16max-active: 32min-idle: 8

配置Redis的常量

/*** 常量* @author dolyw.com* @date 2018/9/3 16:03*/
public class RedisConstant {private RedisConstant() {}public static final String PREFIX_REFRESH_TOKEN = "refresh_token";public static final String PREFIX_ACCESS_TOKEN = "access_token";public static final String PREFIX_SHIRO_EXPIRE = "access_token";public static final String PREFIX_SHIRO_JWT = "shiro:jwt:";}

config自動注入

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Bean(name = "redisTemplate")public RedisTemplate<String, Object> getRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {// 設置序列化Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置redisTemplateRedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);RedisSerializer<?> stringSerializer = new StringRedisSerializer();// key序列化redisTemplate.setKeySerializer(stringSerializer);// value序列化redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// Hash key序列化redisTemplate.setHashKeySerializer(stringSerializer);// Hash value序列化redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
import com.alibaba.excel.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @className: RedisUtil* @description:* @author: sh.Liu* @date: 2022-03-09 14:07*/
// TODO: 2023/7/25 此工具類可以進一步優化
@Component
public class RedisUtil {// 使用jwt的過期時間毫秒private final long defaultTimeout = 1*24*60*60*1000;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 是否存在指定的key** @param key* @return*/public boolean hasKey(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}/*** 刪除指定的key** @param key* @return*/public boolean delete(String key) {return Boolean.TRUE.equals(redisTemplate.delete(key));}//- - - - - - - - - - - - - - - - - - - - -  String類型 - - - - - - - - - - - - - - - - - - - -/*** 根據key獲取值** @param key 鍵* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 將值放入緩存** @param key   鍵* @param value 值* @return true成功 false 失敗*/public void set(String key, String value) {set(key, value, defaultTimeout);}/*** 將值放入緩存并設置時間** @param key   鍵* @param value 值* @param time  時間(秒) -1為無期限* @return true成功 false 失敗*/public void set(String key, String value, long time) {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}}//- - - - - - - - - - - - - - - - - - - - -  object類型 - - - - - - - - - - - - - - - - - - - -/*** 根據key讀取數據*/public Object getObject(final String key) {if (StringUtils.isBlank(key)) {return null;}try {return redisTemplate.opsForValue().get(key);} catch (Exception e) {e.printStackTrace();}return null;}/*** 寫入數據*/public boolean setObject(final String key, Object value) {if (StringUtils.isBlank(key)) {return false;}try {setObject(key, value , defaultTimeout);return true;} catch (Exception e) {e.printStackTrace();}return false;}public boolean setObject(final String key, Object value, long time) {if (StringUtils.isBlank(key)) {return false;}if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}return true;}
}

JWT配置

依賴

        <!--jwt--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.19.0</version></dependency>

配置類

shiro:jwt:# 加密秘鑰secret: f4e2e52034348f86b67cde581c0f9eb5# token有效時長,7天,單位毫秒expire: 604800000header:# 加密算法alg: HS256# token類型typ: JWT

JWT工具類

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {@Value("${shiro.jwt.secret}")private String secret;@Value("${shiro.jwt.expire}")private Long expire;@Value("${shiro.jwt.header.alg}")private String headerAlg;@Value("${shiro.jwt.header.typ}")private String headerTyp;/*** 生成token*/public String getToken(String account, long currentTimeMillis) {// 設置秘鑰StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account).append(secret);// 設置jwt頭headerMap<String, Object> headerClaims = new HashMap<>();headerClaims.put("alg", headerAlg); // 簽名算法headerClaims.put("typ", headerTyp); // token 類型// 設置jwt的header,負載paload以及加密算法String token = JWT.create().withHeader(headerClaims).withClaim("account" ,account).withClaim("expire", currentTimeMillis + expire).sign(Algorithm.HMAC256(stringBuilder.toString()));return token;}/*** 無需秘鑰就能獲取其中的信息* 解析token.* {* "account": "account",* "timeStamp": "134143214"* }*/public Map<String, String> parseToken(String token) {HashMap<String, String> map = new HashMap<String, String>();// 解碼 JWTDecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");map.put("account", account.asString());map.put("expire", expire.asLong().toString());return map;}/*** 解析token獲取賬號.*/public String getAccount(String token) {HashMap<String, String> map = new HashMap<String, String>();DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");return account.asString();}/*** 校驗token是否正確* @param token Token* @return boolean 是否正確*/public boolean verify(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account.asString()).append(secret);// 驗證JWT的簽名和有效性Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());JWTVerifier verifier = JWT.require(algorithm).build();try {verifier.verify(token);return true; // 驗證通過} catch (JWTVerificationException e) {return false; // 驗證失敗}}/*** 校驗token是否過期* @param token Token* @return boolean 是否正確*/public boolean isExpired(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");// 驗證過期時間Long expireTime = expire.asLong();if (System.currentTimeMillis() > expireTime) {return true;}return false;}/*** 獲取token過期時間* @param token Token* @return boolean 是否正確*/public long getExpiredTime(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");return expire.asLong();}}

Shiro配置

ShiroConfig

@Configuration
public class ShiroConfig {/*** 添加自己的過濾器,自定義url規則,* Filter工廠,設置對應的過濾條件和跳轉條件* Shiro自帶攔截器配置規則* 詳情見文檔 http://shiro.apache.org/web.html#urls-** @date 2018/8/31 10:57*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 添加自己的過濾器并且取名為jwtMap<String, Filter> filterMap = new HashMap<>();filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//        // 登出
//        filterChainDefinitionMap.put("/logout", "logout");// 登錄頁面可以匿名訪問filterChainDefinitionMap.put("/sys/login", "anon");
//        // 首頁需要身份驗證后才能訪問
//        filterChainDefinitionMap.put("/index", "authc");
//        // 錯誤頁面,認證不通過跳轉
//        filterChainDefinitionMap.put("/error", "authc");
//        // 其他頁面需要具有 admin 角色才能訪問
//        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//        // 其他頁面需要具有 user:create 權限才能訪問
//        filterChainDefinitionMap.put("/sys/login", "perms[sys:login]");
//        // 其他頁面需要具有 user:update 和 user:delete 權限才能訪問
//        filterChainDefinitionMap.put("/user/manage", "perms[\"user:update,user:delete\"]");filterChainDefinitionMap.put("/**", "jwt");  // /**,一般放在最下,表示對所有資源起作用,使用JwtFiltershiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(userRealm);//關閉shiro的session(無狀態的方式使用shiro)DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);defaultWebSecurityManager.setSubjectDAO(subjectDAO);return defaultWebSecurityManager;}// 將自己的驗證方式加入容器@Beanpublic UserRealm userRealm() {return new UserRealm();}/*開啟注解的權限控制@RequiresAuthentication(標識用戶必須在當前會話中進行了身份驗證(登錄)才能訪問被注解的方法或類)@RequiresUser(標識用戶必須在應用程序中進行了身份驗證(不一定是當前會話)才能訪問被注解的方法或類)@RequiresGuest(標識用戶必須是一個“guest”(未經身份驗證)才能訪問被注解的方法或類)@RequiresRoles(標識用戶必須具有指定的角色才能訪問被注解的方法或類。可以指定一個或多個角色,如 @RequiresRoles("admin") 或 @RequiresRoles({"admin", "user"}))@RequiresPermissions(標識用戶必須具有指定的權限才能訪問被注解的方法或類。可以指定一個或多個權限,如 @RequiresPermissions("user:create") 或 @RequiresPermissions({"user:create", "user:update"}))@RequiresGuest(標識用戶必須是一個“guest”(未經身份驗證)才能訪問被注解的方法或類)*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}/*** 由Spring管理 Shiro的生命周期*/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Bean@DependsOn("lifecycleBeanPostProcessor")public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();// 強制使用cglib,防止重復代理和可能引起代理出錯的問題defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}
}

Realm配置

import com.hebut.demo.common.constant.RedisConstant;
import com.hebut.demo.common.utils.JwtUtil;
import com.hebut.demo.common.utils.RedisUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;import java.util.HashSet;
import java.util.Set;/*** 自定義Realm 處理登錄 權限* * @author ruoyi*/
public class UserRealm extends AuthorizingRealm
{@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate JwtUtil jwtUtil;// 這個方法要重寫,debug源碼得知shiro會判斷token的類型是不是自己支持的類型,不重寫的話會報錯@Overridepublic boolean supports(AuthenticationToken authenticationToken) {return authenticationToken instanceof JwtToken;}/*** 授權*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){// 獲取第一個身份(用戶信息),由于在doGetAuthenticationInfo方法中返回的對象中principal參數傳入的是token,所以這里獲得的也是tokenString token = (String) arg0.getPrimaryPrincipal();String account = jwtUtil.getAccount(token);// 角色列表Set<String> roles = new HashSet<String>();// 根據賬號從數據庫或者其他數據源獲取角色信息(這個操作省略不寫了)// 比如查詢到用戶有一個admin的角色,在這里添加roles.add("admin");// 功能權限Set<String> menus = new HashSet<String>();// 根據賬號從數據庫或者其他數據源獲取權限信息(這個操作省略不寫了)// 比如查詢到用戶有一個sys:select的權限,在這里添加menus.add("sys:select");SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();// 管理員擁有所有權限info.setRoles(roles);info.setStringPermissions(menus);return info;}/*** 登錄認證,自定義登錄認證方式:賬號是否存在,token是否過期* 重寫之后,如果需要登錄認證的接口就會自動調用此接口進行登錄認證*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{System.out.println("登錄認證");// JwtToken中重寫了這個方法了String token = (String) authenticationToken.getCredentials();// 判斷token是否有效(這里只是驗證了簽名是否有效)if (!jwtUtil.verify(token)){return null;}String account = jwtUtil.getAccount(token);if (jwtUtil.isExpired(token)){// 如果過期了,去redis里面查看refreshtime(這里沒有實現這個步驟,直接過期了就退出)return null;}else {// 沒有過期則判斷token是否和redis里面存儲的相等// 獲取accessTokenString accessToken = redisUtil.getObject(RedisConstant.PREFIX_SHIRO_JWT + account + RedisConstant.PREFIX_ACCESS_TOKEN).toString();if(token.equals(accessToken)){// 認證通過則返回認證信息return new SimpleAuthenticationInfo(token, token, getName());}return null;}}}

重寫AuthenticationToken

import org.apache.shiro.authc.AuthenticationToken;/*** @author: lhy* 自定義的shiro接口token,可以通過這個類將string的token轉型成AuthenticationToken,可供shiro使用* 注意:需要重寫getPrincipal和getCredentials方法,因為是進行三件套處理的,沒有特殊配置shiro無法通過這兩個         方法獲取到用戶名和密碼,需要直接返回token,之后交給JwtUtil去解析獲取。(當然了,可以對realm進行配   置HashedCredentialsMatcher,這里就不這么處理了)*/
public class JwtToken implements AuthenticationToken {private String token;public JwtToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}

繼承BasicHttpAuthenticationFilter并重寫,用于每次訪問后端的時候做登錄認證

import com.hebut.demo.common.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;/*** @author: lhy*  jwt過濾器,作為shiro的過濾器,對請求進行攔截并處理跨域配置不在這里配了,我在另外的配置類進行配置了,這里把重心放在驗證上*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter{/*** 過濾器攔截請求的入口方法*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {try {return executeLogin(request, response);  //token驗證} catch (Exception e) {e.printStackTrace();return false;}}/*** 進行token的驗證*/@Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {//在請求頭中獲取tokenHttpServletRequest httpServletRequest = (HttpServletRequest) request;String token = httpServletRequest.getHeader("Authorization"); //前端命名Authorization//token不存在if(token == null || "".equals(token)){return false;}//token存在,進行驗證JwtToken jwtToken = new JwtToken(token);getSubject(request, response).login(jwtToken);  //通過subject,提交給myRealm進行登錄驗證return true;}/*** isAccessAllowed()方法返回false,即認證不通過時進入onAccessDenied方法*/
//    @Override
//    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//        return super.onAccessDenied(request, response);
//    }/*** token認證executeLogin成功后,進入此方法,可以進行token更新過期時間*/
//    @Override
//    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {//    }
}

controller層異常攔截

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;// 用于攔截controller中拋出的異常
// 使用樣例:@ControllerAdvice(basePackages="org.my.pkg")掃描此包下面所有的controller
@ControllerAdvice
public class GlobalExceptionHandler {// 攔截AuthorizationException異常@ExceptionHandler(AuthorizationException.class)@ResponseBodypublic String handleAuthorizationException(AuthorizationException e) {return "沒有通過權限驗證!";}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/16837.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16837.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16837.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

生命在于學習——Python人工智能原理(2.1)

二、機器學習 1、機器學習的定義 機器學習是指從有限的觀測數據中學習出具有一般性的規律&#xff0c;并利用這些規律對未知數據進行預測的方法&#xff0c;通俗的講&#xff0c;機器學習就是讓計算機從數據中進行自動學習&#xff0c;得到某種知識。 傳統的機器學習主要關注…

1分鐘快速掌握JSON格式

文章目錄 先說理論代碼舉例對象型數組型總結 先說理論 下面是JSON的幾種簡單數據類型: 數據類型描述數字型JavaScript中的雙進度浮點類型&#xff0c;通常根據具體情況定義&#xff0c;這里是沒有特殊的整形的。字符串型帶雙引號的Unicode&#xff0c;帶反斜杠轉義布爾型true…

圖形學初識--雙線性插值算法

文章目錄 為什么需要雙線性插值算法&#xff1f;雙線性插值算法是什么&#xff1f;如何雙線性插值&#xff1f;結尾&#xff1a;喜歡的小伙伴可以點點關注贊哦 為什么需要雙線性插值算法&#xff1f; ChatGP回答&#xff1a; 雙線性插值&#xff08;bilinear interpolation&am…

AI繪畫圖生圖有什么用?

隨著AI滲透到我們生活中的各個角落&#xff0c;AI繪畫圖生圖的出現&#xff0c;更是在藝術領域引起了廣泛的關注和討論。那么&#xff0c;AI繪畫圖生圖究竟有什么作用呢? 首先&#xff0c;AI繪畫圖生圖能夠極大地提高創作效率。傳統的繪畫過程需要藝術家們花費大量的時間和精力…

2024年怎么下載學浪app視頻

想要在2024年緊跟潮流&#xff0c;成為一名優秀的學浪用戶嗎&#xff1f;今天就讓我們一起探索如何下載學浪app視頻吧&#xff01; 學浪視頻下載工具打包 學浪下載工具打包鏈接&#xff1a;百度網盤 請輸入提取碼 提取碼&#xff1a;1234 --來自百度網盤超級會員V10的分享…

第14章-藍牙遙控小車 手把手做藍牙APP遙控小車 藍牙串口通訊講解

本文講解手機藍牙如何遙控小車&#xff0c;如何編寫串口通信指令 第14章-手機遙控功能 我們要實現藍牙遙控功能&#xff0c;藍牙遙控功能要使用:1.單片機的串口、2.藍牙通信模塊 所以我們先調試好:單片機的串口->藍牙模塊->接到一起聯調 14.1-電腦控制小車 完成功能…

【面試題】MySQL高頻面試題

談一談你對數據庫的理解&#xff1f; 數據庫是一個用于存儲和管理數據的工具&#xff0c;它提供了一種結構化的方式來組織和訪問數據。數據庫可以存儲大量的數據&#xff0c;并且可以通過查詢語言進行檢索、更新和刪除數據。 數據庫的主要目的是提供一個可靠的數據存儲和管理…

使用docker的常用命令示例

命令描述示例注釋docker run創建并啟動一個容器docker run -d -p 80:80 nginx-d代表后臺運行&#xff0c;-p代表端口映射docker ps列出正在運行的容器docker ps無docker ps -a列出所有容器&#xff0c;包括停止的docker ps -a-a代表列出所有容器docker stop停止一個或多個正在運…

網絡工程師備考2——vlan

vlan 1、什么是VLAN&#xff1f; VLAN&#xff08;Virtual LAN&#xff09;&#xff0c;翻譯成中文是“虛擬局域網”。LAN可以是由少數幾臺家用計算機構成的網絡&#xff0c;也可以是數以百計的計算機構成的企業網絡。VLAN所指的LAN特指使用路由器分割的網絡——也就是廣播域…

簡述 Vue父子組件和非父子組件的通信

Vue組件之間的通信可以分為父子組件通信和非父子組件通信兩大類。下面將分別進行詳細的解釋&#xff1a; 父子組件通信 1. 父傳子 方式&#xff1a;通過props屬性進行傳遞。步驟&#xff1a; 在父組件中定義要傳遞的數據。在父組件的模板中&#xff0c;使用子組件標簽并動態…

ABeam 德碩 Team Building | SDC Green Day——環保公益行動

山野好拾光 春日公益行 繼上年度大連辦公室Green Day活動的順利舉辦&#xff0c;環保的理念更加深入到ABeam每一位員工的心中。春日天氣晴好&#xff0c;西安辦公室的小伙伴們也迫不及待來上一場說走就走的Green Day Outing活動。 本次環保公益行動主題為「夏日Go Green暢享山…

千億級開源大模型Qwen110B部署實測

近日&#xff0c;通義千問團隊震撼開源 Qwen1.5 系列首個千億參數模型 Qwen1.5-110B-Chat。 千億級大模型普通顯卡是跑不了推理的&#xff0c;普通人一般也沒辦法本地運行千億級大模型。 為了探索千億級大模型到底需要計算資源&#xff0c;我用云計算資源部署了Qwen1.5-110B-…

谷歌AI搜索功能“翻車”,用戶體驗引擔憂

近期&#xff0c;谷歌對其搜索引擎進行重大更新&#xff0c;推出了全新AI搜索功能“AI Overview”&#xff0c;試圖通過人工智能技術提供更智能便捷的搜索體驗&#xff0c;并追趕微軟和OpenAI等競爭對手。然而事與愿違&#xff0c;這項備受期待的功能上線后卻頻頻出錯&#xff…

測試基礎06:軟件產品的運行環境dev、sit、test、fat、uat、pre、pro

???????課程大綱 1、Dev開發環境 &#xff08;Development environment&#xff09; 使用者 開發人員使用。 用途 用于編程&#xff0c;版本變動很大。 外部能否訪問 外部用戶無法訪問。 2、sit/ITE系統集成測試環境 &#xff08;System Integration Testing en…

WIFI——ESP8266的一些知識

ESP8266的三種無線通訊模式&#xff1a; AP模式&#xff1a;ESP8266產生WIFI&#xff0c;其他設備加入該wifi 無線終端模式&#xff1a;別人創建wifi&#xff0c;ESP8266加入該wifi 混合模式&#xff1a;雖然是以上兩種都能用&#xff0c;但同一時間只能用其中一個 設置AP模…

大數據的存儲和處理面臨哪些挑戰,如何應對?

大數據的存儲和處理面臨以下挑戰&#xff1a; 數據量巨大&#xff1a;大數據的特點之一是數據量非常龐大&#xff0c;存儲和處理這么大規模的數據是一個挑戰。傳統的數據庫系統可能無法滿足大數據需求&#xff0c;需要尋找適合大規模數據處理的解決方案。 數據異構性&#xff…

30多萬漢字詞語押韻查詢ACCESS\EXCEL數據庫

押韻&#xff0c;也作“壓韻”。作詩詞曲賦等韻文時在句末或聯末用同韻的字相押&#xff0c;稱為押韻。詩歌押韻&#xff0c;使作品聲韻和諧&#xff0c;便于吟誦和記憶&#xff0c;具有節奏和聲調美。舊時押韻&#xff0c;要求韻部相同或相通&#xff0c;也有少數變格。現代新…

《開發問題解決》Window下7z解壓:cannot create symbolic link : 客戶端沒有所需的特權

問題描述&#xff1a; 今天使用7z來解壓東西的是突然出現這個問題。 問題解決&#xff1a; download直接下載到c盤中&#xff0c;由于所在文件夾有權限限制。無法進行正常解壓。 7.zip解壓時使用管理員權限進行解壓&#xff0c;解壓時使用管理員權限。即如圖 使用管理員權限重…

【面試干貨】找出一個偶數能夠表示為兩個素數之和的所有可能情況

【面試干貨】找出一個偶數能夠表示為兩個素數之和的所有可能情況 1、實現思想2、代碼實現 &#x1f496;The Begin&#x1f496;點點關注&#xff0c;收藏不迷路&#x1f496; 1、實現思想 功能&#xff1a;通過循環遍歷奇數&#xff0c;找出一個大于等于 6 的偶數能夠表示為兩…

【C++初階】auto關鍵字

目錄 1.auto簡介 2.auto的使用 1.auto簡介 在早期C/C中auto的含義是&#xff1a;使用auto修飾的變量&#xff0c;是具有自動存儲器的局部變量&#xff0c;但遺憾的 是一直沒有人去使用它&#xff0c;大家可思考下為什么&#xff1f; C11中&#xff0c;標準委員會賦予了auto全…