#Java #shiro #鉴权 #J2EE #Spring
Spring Boot集成Shiro+JWT实现权限控制
1. 项目依赖配置
在pom.xml中添加必要的依赖:
xml1<dependencies>2 <!-- Shiro Spring -->3 <dependency>4 <groupId>org.apache.shiro</groupId>5 <artifactId>shiro-spring</artifactId>6 <version>1.9.0</version>7 </dependency>8 9 <!-- JWT -->10 <dependency>11 <groupId>com.auth0</groupId>12 <artifactId>java-jwt</artifactId>13 <version>4.3.0</version>14 </dependency>15</dependencies>
2. 核心组件说明
2.1 JWTToken
实现AuthenticationToken接口,用于封装JWT令牌:
- 用于替代Shiro默认的UsernamePasswordToken
- 实现getPrincipal()和getCredentials()方法
- 用于JWT令牌的身份验证
示例:
Java1public class JwtToken implements AuthenticationToken { 2 private String token; 3 4 public JwtToken(String token) { 5 this.token = token; 6 } 7 8 @Override 9 public Object getPrincipal() { 10 return token; 11 } 12 13 @Override 14 public Object getCredentials() { 15 return token; 16 } 17}
2.2 JWTUtil
JWT工具类,提供以下功能:
- createToken(): 创建JWT令牌
- getUsername(): 从令牌中获取用户名
- verify(): 验证令牌有效性
- 支持令牌过期时间设置
示例:
Java1@Component 2public class JwtUtil { 3 @Value("${jwt.secret:hasuneMiku}") 4 private String secret; 5 6 @Value("${jwt.expiration:86400}") 7 private Long expiration; 8 9 public String generateToken(String username) { 10 return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact(); 11 } 12 13 public String generateToken(String userId, String userName) { 14 return Jwts.builder().setSubject(userId).setSubject(userName).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact(); 15 } 16 17 public String getUserNameFromToken(String token) { 18 return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody().getSubject(); 19 } 20 21 public boolean validateToken(String token) throws JwtException { 22 try { 23 Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); 24 return true; 25 } catch (JwtException e) { 26 return false; 27 } 28 } 29 30 private SecretKey getSigningKey() { 31 byte[] keyBytes = Decoders.BASE64.decode(secret.replace("\r\n", "")); 32 return Keys.hmacShaKeyFor(keyBytes); 33 } 34 35 36}
2.3 JWTFilter
继承BasicHttpAuthenticationFilter,实现JWT过滤:
- isLoginAttempt(): 判断是否是登录请求
- executeLogin(): 执行登录逻辑
- isAccessAllowed(): 判断是否允许访问
- 从请求头中获取Authorization信息
示例:
Java1public class JWTFilter extends BasicHttpAuthenticationFilter { 2 3 @Override 4 protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { 5 HttpServletRequest req = (HttpServletRequest) request; 6 String authorization = req.getHeader("Authorization"); 7 return authorization != null; 8 } 9 10 @Override 11 protected boolean executeLogin(ServletRequest request, ServletResponse response) { 12 HttpServletRequest req = (HttpServletRequest) request; 13 String token = req.getHeader("Authorization"); 14 JwtToken jwtToken = new JwtToken(token); 15 getSubject(request, response).login(jwtToken); 16 return true; 17 } 18 19 @Override 20 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { 21 if (isLoginAttempt(request, response)) { 22 try { 23 executeLogin(request, response); 24 return true; 25 } catch (Exception e) { 26 return false; 27 } 28 } 29 return false; 30 } 31}
2.4 CustomRealm
自定义Realm实现,继承AuthorizingRealm:
- doGetAuthorizationInfo(): 处理授权逻辑
- doGetAuthenticationInfo(): 处理认证逻辑
- supports(): 支持JWT令牌验证
示例:
Java1public class JwtRealm extends AuthorizingRealm { 2 @Autowired 3 private UserService userService; 4 5 @Autowired 6 private PermService permService; 7 8 @Autowired 9 private RoleService roleService; 10 11 @Autowired 12 private JwtUtil jwtUtil; 13 14 @Override 15 public boolean supports(AuthenticationToken token) { 16 return token instanceof JwtToken; 17 } 18 19 @Override 20 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 21 String username = principals.toString(); 22 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 23 // 获取用户权限 24 Set<String> permissions = permService.getPermByuserName(username); 25 Set<String> roles = roleService.getRolesByUserName(username); 26 info.setStringPermissions(permissions); 27 info.setRoles(roles); 28 return info; 29 } 30 31 @Override 32 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) 33 34 throws AuthenticationException { 35 String token = (String) authenticationToken.getCredentials(); 36 String username = jwtUtil.getUserNameFromToken(token); 37 38 if (username == null || !jwtUtil.validateToken(token)) { 39 throw new AuthenticationException("token invalid"); 40 } 41 42 // 验证用户是否存在 43 if (!userService.exist(username)) { 44 throw new UnknownAccountException("User doesn't exist!"); 45 } 46 47 return new SimpleAuthenticationInfo(username, token, getName()); 48 } 49 50}
3. 配置类说明
ShiroConfig配置类包含:
-
ShiroFilterFactoryBean配置:
- 配置JWT过滤器
- 设置过滤规则
- 配置无权限跳转
-
SecurityManager配置:
- 设置自定义Realm
- 关闭Session管理
- 配置为无状态模式
-
开启注解支持:
- 配置DefaultAdvisorAutoProxyCreator
- 配置AuthorizationAttributeSourceAdvisor
- 配置LifecycleBeanPostProcessor
示例:
Java1 2@Configuration 3public class ShiroConfig { 4 5 @Bean 6 public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { 7 ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); 8 factoryBean.setSecurityManager(securityManager); 9 10 // 添加jwt过滤器 11 Map<String, Filter> filterMap = new HashMap<>(); 12 filterMap.put("jwt", new JWTFilter()); 13 factoryBean.setFilters(filterMap); 14 15 // 设置无权限时跳转的url 16 factoryBean.setUnauthorizedUrl("/unauthorized"); 17 18 // 设置过滤规则 19 Map<String, String> filterRuleMap = new HashMap<>(); 20 21 // 登录接口放行 22 filterRuleMap.put("/api/auth/**", "anon"); 23 24 // 其他所有路径都需要jwt验证 25 filterRuleMap.put("/**", "jwt"); 26 factoryBean.setFilterChainDefinitionMap(filterRuleMap); 27 28 return factoryBean; 29 } 30 31 @Bean 32 public DefaultWebSecurityManager securityManager(JwtRealm realm) { 33 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 34 securityManager.setRealm(realm); 35 36 // 关闭shiro自带的session 37 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); 38 DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); 39 defaultSessionStorageEvaluator.setSessionStorageEnabled(false); 40 subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); 41 securityManager.setSubjectDAO(subjectDAO); 42 43 return securityManager; 44 } 45 46 @Bean 47 public JwtRealm realm() { 48 return new JwtRealm(); 49 } 50 51 /** 52 * 开启Shiro的注解支持 53 */ 54 @Bean 55 public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { 56 DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); 57 advisorAutoProxyCreator.setProxyTargetClass(true); 58 return advisorAutoProxyCreator; 59 } 60 61 @Bean 62 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { 63 AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); 64 advisor.setSecurityManager(securityManager); 65 return advisor; 66 } 67 68 @Bean 69 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { 70 return new LifecycleBeanPostProcessor(); 71 } 72}
4. 权限控制使用方式
4.1 注解方式
Java1// 角色控制2@RequiresRoles("admin")3public void adminOperation() {}4 5// 权限控制6@RequiresPermissions("user:read")7public void readOperation() {}8 9// 登录认证10@RequiresAuthentication11public void authenticatedOperation() {}
4.2 编程方式
Java1Subject subject = SecurityUtils.getSubject();2// 检查角色3subject.checkRole("admin");4// 检查权限5subject.checkPermission("user:read");
5. 接口调用流程
-
用户登录:
- 调用登录接口
- 验证用户名密码
- 生成JWT令牌返回
-
请求接口:
- 请求头携带令牌:
Authorization: {token} - JWTFilter进行令牌解析
- CustomRealm进行权限验证
- 返回请求结果
- 请求头携带令牌:
6. UserService接口定义
需要实现以下方法:
Java1public interface UserService {2 // 获取用户权限列表3 Set<String> getPermissions(String username);4 5 // 获取用户角色列表6 Set<String> getRoles(String username);7 8 // 检查用户是否存在9 boolean exists(String username);10 11 // 验证用户密码12 boolean checkPassword(String username, String password);13}
7. 常见问题与解决方案
7.1 登录失败问题
- 问题: 明明用户名密码正确,但始终登录失败
解决方案:
- 检查密码加密方式是否一致
- 确认salt值的使用是否正确
Java1// 正确的密码校验方式2String encryptPassword = new SimpleHash(3 "MD5", // 算法名称4 password, // 原密码5 ByteSource.Util.bytes(salt), // 盐值6 1024 // 迭代次数7).toHex();
7.2 授权失败问题
- 问题: 注解不生效
解决方案:
- 确认已经配置了AOP支持
- 检查是否启用了注解支持
Java1@EnableAspectJAutoProxy2@Configuration3public class ShiroConfig {4 // ...配置代码5}
7.3 Session问题
- 问题: Session频繁失效
解决方案:
- 调整session超时时间
- 确认是否存在集群环境下的session同步问题
Java1sessionManager.setGlobalSessionTimeout(3600000); // 设置为1小时
7.4 跨域问题
- 问题: 跨域请求被拦截
解决方案:
Java
1@Bean2public FilterRegistrationBean corsFilter() {3 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();4 CorsConfiguration config = new CorsConfiguration();5 config.setAllowCredentials(true);6 config.addAllowedOrigin("*");7 config.addAllowedHeader("*");8 config.addAllowedMethod("*");9 source.registerCorsConfiguration("/**", config);10 FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));11 bean.setOrder(0);12 return bean;13}
7.5 性能优化建议
-
缓存配置:
Java1@Bean2public CacheManager cacheManager() {3 EhCacheManager cacheManager = new EhCacheManager();4 cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");5 return cacheManager;6} -
并发性能优化:
Java1@Bean2public SecurityManager securityManager() {3 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();4 // 开启并发验证5 ((DefaultSubjectDAO)securityManager.getSubjectDAO())6 .setSessionStorageEvaluator(new DefaultSessionStorageEvaluator());7 return securityManager;8}
最佳实践建议
- 始终使用安全的密码加密方式
- 合理配置缓存,提高性能
- 正确处理异常,提供友好的错误提示
- 定期清理过期session
- 使用注解进行权限控制时要考虑粒度
- 在生产环境中要禁用开发模式配置