SpringBoot集成Shiro+JWT(Hutool)完整代碼示例
一、背景介紹
1.1 為什么使用Shiro?
Apache Shiro 是一個(gè)強(qiáng)大且易用的 Java 安全框架,提供了認(rèn)證、授權(quán)、加密和會(huì)話管理功能。在現(xiàn)代應(yīng)用開(kāi)發(fā)中,Shiro 因其簡(jiǎn)單性和靈活性而被廣泛采用:
- ??簡(jiǎn)單易用??:相比 Spring Security,Shiro 的 API 更加直觀和簡(jiǎn)單
- ??功能全面??:提供認(rèn)證、授權(quán)、會(huì)話管理、加密等企業(yè)級(jí)安全功能
- ??輕量級(jí)??:不依賴任何容器,可以獨(dú)立運(yùn)行
- ??業(yè)界規(guī)范??:被眾多企業(yè)采用,有豐富的社區(qū)支持和文檔
1.2 為什么需要雙Token?
在原有單Token方案基礎(chǔ)上引入 ??Access Token(訪問(wèn)令牌)?? 和 ??Refresh Token(刷新令牌)?? 的組合,解決以下問(wèn)題:
- ??安全性??:Access Token 短期有效降低泄露風(fēng)險(xiǎn),Refresh Token 獨(dú)立存儲(chǔ)且過(guò)期時(shí)間長(zhǎng)
- ??用戶體驗(yàn)??:自動(dòng)刷新 Access Token,用戶無(wú)感知續(xù)期
- ??合規(guī)性??:符合 OAuth 2.0 標(biāo)準(zhǔn)流程
二、技術(shù)棧組成
技術(shù)組件 | 作用 | 版本要求 |
---|---|---|
SpringBoot | 基礎(chǔ)框架 | 3.x |
Apache Shiro | 認(rèn)證和授權(quán)核心 | 2.0.0+ |
Hutool-JWT | 令牌生成與驗(yàn)證 | 5.8.24+ |
三、環(huán)境準(zhǔn)備
3.1 創(chuàng)建 SpringBoot 項(xiàng)目
<!-- pom.xml --> <dependencies> <!-- Shiro核心依賴 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <classifier>jakarta</classifier> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <classifier>jakarta</classifier> <version>${shiro.version}</version> <exclusions> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </exclusion> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <classifier>jakarta</classifier> <version>${shiro.version}</version> <exclusions> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </exclusion> </exclusions> </dependency> <!-- Hutool-JWT --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.39</version> </dependency> </dependencies>
四、核心代碼實(shí)現(xiàn)
4.1 JWT工具類(JwtUtil.java)
import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTUtil; import cn.hutool.core.date.DateTime; import org.springframework.data.redis.core.RedisTemplate; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class JwtUtil { private static final String TOKEN_KEY = "sys:token:"; private static final long ACCESS_EXPIRE = 1000 * 60 * 15; // 15分鐘 /** * 獲取密鑰(可選,我這里做的是動(dòng)態(tài)配置的,可以根據(jù)需要寫(xiě)死就行) * @return 密鑰 */ private static byte[] getJwtSSecret() { SysParamsService sysParamsService = SpringUtil.getBean(SysParamsService.class); String jwtSecret = sysParamsService.getValue("jwt.secret", true); return jwtSecret.getBytes(); } /** * 獲取刷新token過(guò)期時(shí)間-單位天(可選,我這里做的是動(dòng)態(tài)配置的,可以根據(jù)需要寫(xiě)死就行) * @return 過(guò)期時(shí)間-單位天 */ private static int getRefreshExp() { SysParamsService sysParamsService = SpringUtil.getBean(SysParamsService.class); String refreshExp = sysParamsService.getValue("jwt.exp", true); return Integer.parseInt(refreshExp); } // 生成雙Token public static Map<String, String> generateTokens(Long userId,String username) { Map<String, String> tokens = new HashMap<>(); // Access Token tokens.put("accessToken", createToken(userId,username)); // Refresh Token tokens.put("refreshToken", createRefreshToken(userId)); return tokens; } public static String createToken(Long userId,String username) { Map<String, Object> payload = new HashMap<>(); payload.put("userId", userId); payload.put("username", username); payload.put("type", "access"); payload.put("exp", new DateTime(System.currentTimeMillis() + ACCESS_EXPIRE).getTime()); return JWTUtil.createToken(payload, getJwtSSecret()); } public static String createRefreshToken(Long userId) { Map<String, Object> payload = new HashMap<>(); payload.put("userId", userId); payload.put("type", "refresh"); String refreshToken = JWTUtil.createToken(payload, getJwtSSecret()); RedisTemplate<String, String> redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class); redisTemplate.opsForValue().set(TOKEN_KEY+userId, refreshToken, getRefreshExp(), TimeUnit.DAYS); return refreshToken; } // 刷新Token public static void refreshAccessToken(String refreshToken) { Assert.isTrue(JWTUtil.verify(refreshToken, getJwtSSecret()), "非法Token錯(cuò)誤"); JWT jwt = JWTUtil.parseToken(refreshToken); Assert.isTrue(ObjectUtil.equals("refresh", jwt.getPayload("type")), "非法Token錯(cuò)誤"); Long userId = (Long) jwt.getPayload("userId"); RedisTemplate<String, String> redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class); String redis_refreshToken = redisTemplate.opsForValue().get(TOKEN_KEY + userId); Assert.isTrue(ObjectUtil.equals(redis_refreshToken, refreshToken), "Token已過(guò)期"); //可選 用于延長(zhǎng)緩存時(shí)間 long expire = redisTemplate.getExpire(TOKEN_KEY + userId, TimeUnit.DAYS); if(expire == 0){ redisTemplate.expire(TOKEN_KEY + userId, 7, TimeUnit.DAYS); } } /** * 從Token中獲取用戶Id * @param refreshToken JWT Token字符串 * @return 用戶Id */ public static Long getUserIdFromRefreshToken(String refreshToken) { Assert.isTrue(JWTUtil.verify(refreshToken, getJwtSSecret()), "非法Token錯(cuò)誤"); JWT jwt = JWTUtil.parseToken(refreshToken); Assert.isTrue(ObjectUtil.equals("refresh", jwt.getPayload("type")), "非法Token錯(cuò)誤"); Long userId = (Long) jwt.getPayload("userId"); RedisTemplate<String, String> redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class); long expire = redisTemplate.getExpire(TOKEN_KEY + userId, TimeUnit.DAYS); Assert.isTrue(expire >= 0, "Token已過(guò)期"); String redis_refreshToken = redisTemplate.opsForValue().get(TOKEN_KEY + userId); Assert.isTrue(ObjectUtil.equals(redis_refreshToken, refreshToken), "Token已過(guò)期"); return (Long) jwt.getPayload("userId"); } /** * 從Token中獲取用戶Id * @param token JWT Token字符串 * @return 用戶Id */ public static Long getUserIdFromToken(String token) { Assert.isTrue(JWTUtil.verify(token, getJwtSSecret()), "非法Token錯(cuò)誤"); JWT jwt = JWTUtil.parseToken(token); Assert.isTrue(ObjectUtil.equals(jwt.getPayload("type"),"access"), "非法Token錯(cuò)誤"); Assert.isTrue(jwt.getPayload("exp")!=null && jwt.validate(0),"token已失效"); return (Long) jwt.getPayload("userId"); } /** * 從Token中獲取用戶名 * @param token JWT Token字符串 * @return 用戶名 */ public static String getUsernameFromToken(String token) { Assert.isTrue(JWTUtil.verify(token, getJwtSSecret()), "非法Token錯(cuò)誤"); JWT jwt = JWTUtil.parseToken(token); Assert.isTrue(jwt.getPayload("exp")!=null && jwt.validate(0),"token已失效"); return jwt.getPayload("username").toString(); } }
4.2 Shiro配置類(ShiroConfig.java)
import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.config.ShiroFilterConfiguration; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import jakarta.servlet.Filter; @Configuration public class ShiroConfig { @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionValidationSchedulerEnabled(false); sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } @Bean("securityManager") public SecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(oAuth2Realm); securityManager.setSessionManager(sessionManager); securityManager.setRememberMeManager(null); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) { ShiroFilterConfiguration config = new ShiroFilterConfiguration(); config.setFilterOncePerRequest(true); ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); shiroFilter.setShiroFilterConfiguration(config); Map<String, Filter> filters = new HashMap<>(); // oauth過(guò)濾 filters.put("oauth2", new Oauth2Filter()); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/v3/api-docs/**", "anon"); filterMap.put("/doc.html", "anon"); filterMap.put("/favicon.ico", "anon"); filterMap.put("/refreshToken", "anon"); filterMap.put("/login", "anon"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
4.3 注冊(cè)過(guò)濾器
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<DelegatingFilterProxy> shiroFilterRegistration() { FilterRegistrationBean<DelegatingFilterProxy> registration = new FilterRegistrationBean<>(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); // 該值缺省為false,表示生命周期由SpringApplicationContext管理,設(shè)置為true則表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } }
4.4 注冊(cè)oauth2過(guò)濾器
public class Oauth2Filter extends AuthenticatingFilter { private static final Logger logger = LoggerFactory.getLogger(Oauth2Filter.class); @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { // 獲取請(qǐng)求token String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { logger.warn("createToken:token is empty"); return null; } return new Oauth2Token(token); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 獲取請(qǐng)求token,如果token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { logger.warn("onAccessDenied:token is empty"); HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED)); httpResponse.getWriter().print(json); return false; } return executeLogin(request, response); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { Throwable throwable = e.getCause() == null ? e : e.getCause(); Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED, throwable.getMessage()); String json = JsonUtils.toJsonString(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 獲取請(qǐng)求的token */ private String getRequestToken(HttpServletRequest httpRequest) { String token = null; // 從header中獲取token String authorization = httpRequest.getHeader(Constant.AUTHORIZATION); if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) { token = authorization.replace("Bearer ", ""); } return token; } }
4.5 認(rèn)證類
import java.util.HashSet; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; @Component public class Oauth2Realm extends AuthorizingRealm { @Resource private UserService userService; private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class); @Override public boolean supports(AuthenticationToken token) { return token instanceof Oauth2Token; } /** * 授權(quán)(驗(yàn)證權(quán)限時(shí)調(diào)用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { UserDetail user = (UserDetail) principals.getPrimaryPrincipal(); // 用戶權(quán)限列表 Set<String> permsSet = new HashSet<>(); if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) { permsSet.add("sys:role:superAdmin"); permsSet.add("sys:role:normal"); } else { permsSet.add("sys:role:normal"); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(permsSet); return info; } /** * 認(rèn)證(登錄時(shí)調(diào)用) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String accessToken = (String) token.getPrincipal(); // 根據(jù)accessToken,查詢用戶信息 Long userId = JwtUtil.getUserIdFromToken(accessToken); Assert.notNull(userId, "token已過(guò)期,請(qǐng)重新登入"); // 查詢用戶信息 SysUserEntity userEntity = userService.getUser(userId); // 轉(zhuǎn)換成UserDetail對(duì)象 UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDetail, accessToken, getName()); return info; } }
4.6 token類
public class Oauth2Token implements AuthenticationToken { private String token; public Oauth2Token(String token) { this.token = token; } @Override public String getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
4.7 Shiro獲取用戶信息工具類
public class SecurityUser { public static Subject getSubject() { try { return SecurityUtils.getSubject(); } catch (Exception e) { return null; } } /** * 獲取用戶信息 */ public static UserDetail getUser() { Subject subject = getSubject(); if (subject == null) { return new UserDetail(); } UserDetail user = (UserDetail) subject.getPrincipal(); if (user == null) { return new UserDetail(); } return user; } public static String getToken() { return getUser().getToken(); } /** * 獲取用戶ID */ public static Long getUserId() { return getUser().getId(); } }
五、雙Token實(shí)現(xiàn)原理
5.1 Token結(jié)構(gòu)對(duì)比
Token類型 | 有效期 | 存儲(chǔ)位置 | 包含信息 |
---|---|---|---|
Access Token | 15分鐘 | 客戶端 | 用戶ID、用戶名稱,類型,過(guò)期時(shí)間,角色(可選)、權(quán)限(可選) |
Refresh Token | 7天 | 客戶端,Redis | 用戶ID、類型 |
5.2 核心流程圖
六、完整代碼示例
6.1 登錄控制器(AuthController.java)
@RestController public class AuthController { @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { User user = userService.findByUsername(request.getUsername()); if (user == null || !user.getPassword().equals(request.getPassword())) { return Result.error("賬號(hào)或密碼錯(cuò)誤"); } String accessToken = JwtUtil.createAccessToken( user.getUsername(), user.getId() ); String refreshToken = JwtUtil.createRefreshToken(user.getUsername(),user.getId()); return Result.success(Map.of( "accessToken", accessToken, "refreshToken", refreshToken )); } @PostMapping("/refreshToken") public Result<String> refreshToken(@RequestHeader("refreshToken") String refreshToken) { Long userId = JwtUtil.getUserIdFromRefreshToken(refreshToken); //if(userId==null)userId=1904748826795986946L; SysUserDTO user = sysUserService.getByUserId(userId); Assert.notNull(user, "token異常,非法登入"); String newAccessToken = JwtUtil.createToken(user.getId(),user.getUsername()); JwtUtil.refreshAccessToken(refreshToken);//自動(dòng)續(xù)租 return new Result<String>().ok(newAccessToken); } }
七、雙Token優(yōu)勢(shì)總結(jié)
??維度?? | ??單Token方案?? | ??雙Token方案?? |
---|---|---|
??安全性?? | 單一Token泄露風(fēng)險(xiǎn)高 | Access Token短期有效,Refresh Token雙存儲(chǔ)(可自動(dòng)延遲過(guò)期時(shí)間,并保證安全性) |
??用戶體驗(yàn)?? | 頻繁登錄/重新認(rèn)證 | 自動(dòng)續(xù)期,用戶無(wú)感知 |
??合規(guī)性?? | 不符合OAuth2.0標(biāo)準(zhǔn) | 完全遵循OAuth2.0標(biāo)準(zhǔn)流程 |
八 、補(bǔ)充
為確保系統(tǒng)的安全性,本方案采用了??單刷新Token綁定機(jī)制??。具體而言,每個(gè)用戶的刷新Token(Refresh Token)與單一設(shè)備或終端綁定。當(dāng)同一用戶在其他設(shè)備或終端上登錄時(shí),新的登錄操作將導(dǎo)致之前設(shè)備的刷新Token失效(通常伴隨Access Token的到期登入失效)。這種機(jī)制有效防止了同一賬戶在多設(shè)備間的異常并行登錄,增強(qiáng)了賬戶的安全性。
然而,根據(jù)不同的業(yè)務(wù)需求和安全策略,開(kāi)發(fā)者可以根據(jù)實(shí)際情況對(duì)Token管理機(jī)制進(jìn)行調(diào)整。例如,若業(yè)務(wù)場(chǎng)景允許多設(shè)備同時(shí)在線,可以修改Token綁定策略,支持多刷新Token共存。此外,開(kāi)發(fā)者們也歡迎在下方評(píng)論區(qū)提出自己的建議,共同探討和優(yōu)化。
到此這篇關(guān)于SpringBoot集成Shiro+JWT(Hutool)完整代碼示例的文章就介紹到這了,更多相關(guān)SpringBoot Shiro JWT集成內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)上傳網(wǎng)絡(luò)圖片到微信臨時(shí)素材
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)上傳網(wǎng)絡(luò)圖片到微信臨時(shí)素材,網(wǎng)絡(luò)圖片上傳到微信服務(wù)器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07Java源碼解析ThreadLocal及使用場(chǎng)景
今天小編就為大家分享一篇關(guān)于Java源碼解析ThreadLocal及使用場(chǎng)景,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01四個(gè)Java常見(jiàn)分布式鎖的選型和性能對(duì)比
當(dāng)涉及到分布式系統(tǒng)中的并發(fā)控制和數(shù)據(jù)一致性時(shí),分布式鎖是一種常見(jiàn)的解決方案,本文將對(duì)幾種常見(jiàn)的分布式鎖實(shí)現(xiàn)原理、實(shí)現(xiàn)示例、應(yīng)用場(chǎng)景以及優(yōu)缺點(diǎn)進(jìn)行詳細(xì)分析,需要的可以參考一下2023-05-05詳解JavaWeb如何實(shí)現(xiàn)文件上傳和下載功能
這篇文章主要介紹了如何利用JavaWeb實(shí)現(xiàn)文件的上傳和下載功能,文中的示例代碼講解詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以學(xué)習(xí)一下2021-12-12Java實(shí)現(xiàn)軟件運(yùn)行時(shí)啟動(dòng)信息窗口的方法
這篇文章主要介紹了Java實(shí)現(xiàn)軟件運(yùn)行時(shí)啟動(dòng)信息窗口的方法,比較實(shí)用的功能,需要的朋友可以參考下2014-08-08淺談java中靜態(tài)方法的重寫(xiě)問(wèn)題詳解
本篇文章是對(duì)java中靜態(tài)方法的重寫(xiě)問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06