本文主要是介绍SpringBoot集成Shiro+JWT(Hutool)完整代码示例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《SpringBoot集成Shiro+JWT(Hutool)完整代码示例》ApacheShiro是一个强大且易用的Java安全框架,提供了认证、授权、加密和会话管理功能,在现代应用开发中,Shiro因...
一、背景介绍
1.1 为什么使用Shiro?
Apache Shiro 是一个强大且易用的 Java 安全框架,提供了认证、授权、加密和会话管理功能。在现代应用开发中,Shiro 因其简单性和灵活性而被广泛采用:
- 简单易用:相比 Spring Security,Shiro 的 API 更加直观和简单
- 功能全面:提供认证、授权、会话管理、加密等企业级安全功能
- 轻量级:不依赖任何容器,可以独立运行
- 业界规范:被众多企业采用,有丰富的社区支持和文档
1.2 为什么需要双Token?
在原有单Token方案基础上引入 Access Token(访问令牌) 和 Refresh Token(刷新令牌) 的组合,解决以下问题:
- 安全性:Access Token 短期有效降低泄露风险,Refresh Token 独立存储且过期时间长
- 用户体验:自动刷新 Access Token,用户无感知续期
- 合规性:符合 OAuth 2.0 标准流程kKAdR
二、技术栈组成
技术组件 | 作用 | 版本要求 |
---|---|---|
SpringBoot | 基础框架 | 3.x |
Apache Shiro | 认证和授权核心 | 2.0.0+ |
Hutool-JWT | 令牌生成与验证 | 5.8.24+ |
三、环境准备
3.1 创建 SpringBoot 项目
<!-- 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>
四、核心代码实现
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分钟 /** * 获取密钥(可选,我这里做的是动态配置的,可以根据需要写死就行) * @return 密钥 */ private static byte[] getJwtSSecret() { SysParamsService sysParamsService = SpringUtil.getBean(SysParamsService.class); String jwtSecret = sysParamsService.getValue("jwt.secret", true); return jwtSecret.getBytes(); } /** * 获取刷新token过期时间-单位天(可选,我这里做的是动态配置的,可以根据需要写死就行) * @return 过期时间-单位天 */ 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 HashMakKAdRp<>(); // 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错误"); JWT jwt = JWTUtil.parseToken(refreshToken); Assert.isTrue(ObjectUtil.equals("refresh", jwt.getPayload("type")), "非法Token错误"); 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已过期"); //可选 用于延长缓存时间 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错误"); JWT jwt = JWTUtil.parseToken(refreshToken); Assert.isTrue(ObjectUtil.equals("refresh", jwt.getPayload("type")), "非法Token错误"); Long userId = (Long) jwt.getPayload("userId"); RedisTemplate<String, String> redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class); long expire = redisTemplate.getExpire(TOKEN_KEY + userId, TipythonmeUnit.DAYS); Assert.isTrue(expire >= 0, "Token已过期"); String redis_refreshToken = redisTemplate.opsForValue().get(TOKEN_KEY + userId); Assert.isTrue(ObjectUtil.equals(redis_refreshToken, refreshToken), "Token已过期"); 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错误"); JWT jwt = JWTUtil.parseToken(token); Assert.isTrue(ObjectUtil.equals(jwt.getPayload("type"),"access"), "非法Token错误"); 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错误"); 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过滤 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 注册过滤器
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<DelegatingFilterProxy> shiroFilterRegistration() { FilterRegistrationBean<DelegatingFilterProxy> registration = new FilterRegistrationBean<>(); registration.setFilter(new DelegatingFilterProxy("shiroFilter")); // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 registration.addInitParameter("targetFilterLifecycle", "true"); registration.setEnabled(true); php registration.setOrder(Integer.MAX_VALUE - 1); registration.addUrlPatterns("/*"); return registration; } }
4.4 注册oauth2过滤器
public class Oauth2Filter extends AuthenticatingFilter { private static final Logger logger = LoggerFactory.getLogger(Oauth2Filter.class); @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { // 获取请求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 { // 获取请求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; } /** * 获取请求的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 认证类
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;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
UserDetail user = (UserDetail) principajavascriptls.getPrimaryPrincipal();
// 用户权限列表
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;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
// 根据accessToken,查询用户信息
Long userId = JwtUtil.getUserIdFromToken(accessToken);
Assert.notNull(userId, "token已过期,请重新登入");
// 查询用户信息
SysUserEntity userEntity = userService.getUser(userId);
// 转换成UserDetail对象
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实现原理
5.1 Token结构对比
Token类型 | 有效期 | 存储位置 | 包含信息 |
---|---|---|---|
Access Token | 15分钟 | 客户端 | 用户ID、用户名称,类型,过期时间,角色(可选)、权限(可选) |
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("账号或密码错误"); } 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);//自动续租 return new Result<String>().ok(newAccessToken); } }
七、双Token优势总结
维度 | 单Token方案 | 双Token方案 |
---|---|---|
安全性 | 单一Token泄露风险高 | Access Token短期有效,Refresh Token双存储(可自动延迟过期时间,并保证安全性) |
用户体验 | 频繁登录/重新认证 | 自动续期,用户无感知 |
合规性 | 不符合OAuth2.0标准 | 完全遵循OAuth2.0标准流程 |
八 、补充
为确保系统的安全性,本方案采用了单刷新Token绑定机制。具体而言,每个用户的刷新Token(Refresh Token)与单一设备或终端绑定。当同一用户在其他设备或终端上登录时,新的登录操作将导致之前设备的刷新Token失效(通常伴随Access Token的到期登入失效)。这种机制有效防止了同一账户在多设备间的异常并行登录,增强了账户的安全性。
然而,根据不同的业务需求和安全策略,开发者可以根据实际情况对Token管理机制进行调整。例如,若业务场景允许多设备同时在线,可以修改Token绑定策略,支持多刷新Token共存。此外,开发者们也欢迎在下方评论区提出自己的建议,共同探讨和优化。
到此这篇关于SpringBoot集成Shiro+JWT(Hutool)完整代码示例的文章就介绍到这了,更多相关SpringBoot Shiro JWT集成内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!
这篇关于SpringBoot集成Shiro+JWT(Hutool)完整代码示例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!