Spring Boot整合Shiro权限控制:手把手教你用@RequiresPermissions注解保护你的API接口
Spring Boot整合Shiro权限控制实战:从零构建API安全防护体系
在当今企业级应用开发中,权限控制是保障系统安全的核心环节。想象一下这样的场景:你的电商后台管理系统刚刚上线,突然发现普通客服人员能够修改商品价格,或者市场专员意外删除了重要订单数据——这类权限越界问题轻则导致业务混乱,重则引发安全事故。本文将带你从零开始,基于Spring Boot和Apache Shiro构建一套完善的API权限控制系统,重点解析@RequiresPermissions注解的高级应用技巧,让你的接口安全达到生产级标准。
1. 环境准备与基础配置
1.1 项目初始化与依赖引入
首先创建一个标准的Spring Boot项目(2.7.x版本),在pom.xml中添加必要的依赖:
<dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Shiro整合Spring Boot --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.11.0</version> </dependency> <!-- 数据库访问(以MyBatis为例) --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> </dependencies>1.2 Shiro核心配置类
创建ShiroConfig.java配置文件,这是整个权限系统的中枢神经:
@Configuration public class ShiroConfig { @Bean public Realm customRealm() { CustomAuthorizingRealm realm = new CustomAuthorizingRealm(); realm.setCredentialsMatcher(new HashedCredentialsMatcher("SHA-256")); return realm; } @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean(); filterFactory.setSecurityManager(securityManager); filterFactory.setLoginUrl("/api/auth/unauthorized"); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/api/auth/**", "anon"); filterMap.put("/**", "authc"); filterFactory.setFilterChainDefinitionMap(filterMap); return filterFactory; } }提示:在生产环境中,建议使用Redis等缓存方案存储Session和授权信息,避免频繁访问数据库。
2. 自定义Realm实现
2.1 数据库表设计
权限系统的持久层设计直接影响系统的灵活性和性能。以下是推荐的最小化表结构:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(128) NOT NULL, `salt` varchar(64) NOT NULL, `status` tinyint DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ); CREATE TABLE `sys_role` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `description` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `sys_permission` ( `id` bigint NOT NULL AUTO_INCREMENT, `code` varchar(100) NOT NULL COMMENT '权限标识符', `name` varchar(100) NOT NULL, `type` tinyint DEFAULT '1' COMMENT '1:菜单 2:按钮 3:API', PRIMARY KEY (`id`), UNIQUE KEY `idx_code` (`code`) ); -- 关联表 CREATE TABLE `sys_user_role` ( `user_id` bigint NOT NULL, `role_id` bigint NOT NULL, PRIMARY KEY (`user_id`,`role_id`) ); CREATE TABLE `sys_role_permission` ( `role_id` bigint NOT NULL, `permission_id` bigint NOT NULL, PRIMARY KEY (`role_id`,`permission_id`) );2.2 自定义Realm核心逻辑
实现AuthorizingRealm的子类,完成认证和授权逻辑:
public class CustomAuthorizingRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } if (user.getStatus() == 0) { throw new LockedAccountException("账号已被禁用"); } return new SimpleAuthenticationInfo( user.getId(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Long userId = (Long) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 获取用户角色集合 Set<String> roles = userService.findRolesByUserId(userId); info.setRoles(roles); // 获取用户权限集合 Set<String> permissions = userService.findPermissionsByUserId(userId); info.setStringPermissions(permissions); return info; } }3. @RequiresPermissions注解深度解析
3.1 基础用法与权限字符串设计
@RequiresPermissions注解支持多种权限字符串格式,根据业务复杂度可灵活选择:
@RestController @RequestMapping("/api/products") public class ProductController { // 简单权限控制 @RequiresPermissions("product:view") @GetMapping public List<Product> listProducts() { return productService.findAll(); } // 多操作类型控制 @RequiresPermissions("product:create") @PostMapping public Product createProduct(@RequestBody Product product) { return productService.save(product); } // 实例级权限控制(ID为动态参数) @RequiresPermissions("product:edit:#{productId}") @PutMapping("/{productId}") public Product updateProduct(@PathVariable Long productId, @RequestBody Product product) { return productService.update(productId, product); } // 多权限逻辑组合(AND或OR) @RequiresPermissions(value = {"product:delete", "audit:record"}, logical = Logical.OR) @DeleteMapping("/{productId}") public void deleteProduct(@PathVariable Long productId) { productService.delete(productId); } }3.2 权限字符串最佳实践
根据多年项目经验,推荐以下权限字符串设计规范:
| 层级 | 示例 | 说明 |
|---|---|---|
| 模块级 | product:* | 对整个产品模块有全部权限 |
| 操作级 | product:create | 仅能创建产品 |
| 实例级 | product:edit:123 | 仅能编辑ID为123的产品 |
| 字段级 | product:price:update | 仅能修改产品价格字段 |
注意:权限字符串的设计应当与业务需求保持同步演进,初期可以粗粒度控制,随着业务复杂度的提升逐步细化。
4. 高级技巧与实战经验
4.1 动态权限注入技术
在某些场景下,我们需要根据运行时条件动态决定权限要求。Shiro支持通过SpEL表达式实现这一需求:
@RequiresPermissions("order:view:#{order.customerId}") @GetMapping("/orders/{orderId}") public Order getOrderDetails(@PathVariable Long orderId) { Order order = orderService.findById(orderId); // 将order对象放入模型,供权限表达式使用 SecurityUtils.getSubject().getSession() .setAttribute("order", order); return order; }4.2 自定义权限验证逻辑
当默认的权限解析规则不满足需求时,可以扩展PermissionResolver:
public class CustomPermissionResolver implements PermissionResolver { @Override public Permission resolvePermission(String permissionString) { if (permissionString.startsWith("special:")) { return new SpecialPermission(permissionString); } return new WildcardPermission(permissionString); } } // 在Realm中设置自定义解析器 realm.setPermissionResolver(new CustomPermissionResolver());4.3 权限异常统一处理
通过Spring的@ControllerAdvice实现全局异常捕获,提供友好的错误响应:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(AuthorizationException.class) @ResponseBody public ResponseEntity<ApiResponse> handleShiroException(AuthorizationException e) { ApiResponse response = new ApiResponse(); response.setCode(403); if (e instanceof UnauthorizedException) { response.setMessage("当前操作需要权限: " + ((UnauthorizedException)e).getUnauthorizedPermissions()); } else { response.setMessage("无权访问该资源"); } return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); } }4.4 性能优化策略
在高并发场景下,权限验证可能成为性能瓶颈。以下是经过验证的优化方案:
缓存授权信息:配置Shiro的缓存管理器,减少数据库查询
@Bean public CacheManager redisCacheManager() { RedisCacheManager cacheManager = new RedisCacheManager(); cacheManager.setExpire(1800); // 30分钟过期 return cacheManager; } // 在SecurityManager中设置 securityManager.setCacheManager(redisCacheManager());批量权限预验证:对于批量操作,提前验证所有权限
Subject subject = SecurityUtils.getSubject(); String[] permissions = productIds.stream() .map(id -> "product:edit:" + id) .toArray(String[]::new); if (!subject.isPermittedAll(permissions)) { throw new AuthorizationException(); }权限热加载:当用户权限变更时,主动清除缓存
public void clearAuthorizationCache(Long userId) { Cache<Object, AuthorizationInfo> cache = redisCacheManager().getCache("authorizationCache"); cache.remove(userId); }
5. 测试与调试技巧
5.1 单元测试方案
使用Spring Boot Test框架编写权限测试用例:
@SpringBootTest @AutoConfigureMockMvc class ProductControllerTest { @Autowired private MockMvc mockMvc; @Test @WithMockUser(username = "admin", authorities = {"product:view"}) void shouldAccessWhenHasPermission() throws Exception { mockMvc.perform(get("/api/products")) .andExpect(status().isOk()); } @Test @WithMockUser(username = "guest", authorities = {"product:read"}) void shouldDenyWhenNoPermission() throws Exception { mockMvc.perform(get("/api/products")) .andExpect(status().isForbidden()); } }5.2 生产环境问题排查
当权限系统出现异常时,可按以下步骤排查:
检查Shiro的DEBUG日志,确认权限验证流程
logging.level.org.apache.shiro=DEBUG验证当前Subject的权限信息是否正确
Subject subject = SecurityUtils.getSubject(); System.out.println("Principal: " + subject.getPrincipal()); System.out.println("Permissions: " + subject.isPermitted("product:view"));检查数据库中的权限数据是否与注解要求匹配
确认自定义Realm的
doGetAuthorizationInfo方法是否被正确调用
在实际项目中,我们曾遇到一个典型问题:由于权限字符串大小写不一致导致验证失败(数据库存储的是"Product:view"而注解要求"product:view")。这类问题可以通过统一规范或自定义权限比较逻辑来解决。
