spring boot + shiro 实现角色权限控制 2023-11-12 程序之旅,记录 1 条评论 1767 次阅读 ## spring boot + shiro 实现角色权限控制 ### 简介 Apache Shiro 是一个强大并且易于使用的java安全框架,可以用与身份验证、授权、加密和会话管理。同样的框架还有spring security,spring security有很好的平台支持和活跃的社区氛围,并且对 spring 完美兼容,但是使用难度上,远远超过shiro。 - 身份认证:用户身份识别。 - 授权:用户权限控制。知道来的人有没有资格进来。 - 加密:加密敏感数据,防止偷窥。比如密码md5两次加密。 - 会话管理:用户的时间敏感的状态信息。 > shiro上手快 ,控制粒度可糙可细 ,自由度高,可以独立运行。 ### 类说明 | 类名 | 说明 | | --------------- | ------------------------------------------------------------ | | Subject | 可以理解为与shiro打交道的对象,该对象封装了一些对方的信息,shiro可以通过subject拿到这些信息 | | SecurityManager | Shiro的总经理,通过指使Authorizer和Authenticator等对subject进行授权和身份验证等工作 | | Realm | 管理着一些如用户、角色、权限等重要信息,Shiro中所需的这些重要信息都是从Realm这里获取的,Realm本质上就是一个重要信息的数据源 | | Authenticator | 认证器,负责Subject的认证操作,认证过程就是根据Subject提供的信息通过Realm查询到相关信息,然后做对比,支持扩展 | | Authorizer | 授权器,控制着Subject对服务资源的访问权限 | | SessionManager | 用于管理Session,这个Session可以是web的也可以不是web的 | | SessionDao | 把Session的 CRUD和存储介质联系起来的工具,存储介质可以是数据库,也可以是缓存,比如把session放到redis里面 | | CacheManager | 缓存控制器,Realm管理的数据(用户、角色、权限)可以放到缓存里由CacheManager管理,提高认证授权等的速度 | | Cryptography | 加密组件,Shiro提供了很多加解密算法的组件 | Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表Shiro应用安全的四大基石。 ### 实践开发 #### 开发环境 - spring boot 2.3.0.RELEASE - maven 3.6.0 - jdk 1.8 - mysql 8.0 - intellij idea #### 设计技术 spring boot + shiro + swagger + mybatis #### maven ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.0.RELEASE com.felton.springboot shiro 0.0.1-SNAPSHOT shiro Demo project for Spring Boot 1.8 8.0.28 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.apache.shiro shiro-spring 1.4.2 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java ${mysql.version} org.mybatis.generator mybatis-generator-core 1.3.5 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 com.baomidou mybatis-plus-boot-starter 3.1.2 org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-maven-plugin org.mybatis.generator mybatis-generator-maven-plugin 1.3.5 ${basedir}/src/main/resources/mybatis-generator.xml true true mysql mysql-connector-java ${mysql.version} ``` #### application.yml配置 主要是数据库配置 ```xml spring: datasource: url: jdbc:mysql://192.168.2.3:3306/shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver #mybatis mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.felton.springboot.shrio.entity global-config: db-config: logic-delete-value: 0 logic-not-delete-value: 1 refresh: true configuration: map-underscore-to-camel-case: true cache-enabled: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` #### 数据库脚本 数据库表结构,标准的RBAC用户角色权限结构。 ![image-20231111094301453](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/images/image-20231111094301453.png) ```sql -- shiro.permission definition CREATE TABLE `permission` ( `permission_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `permission_name` varchar(255) DEFAULT NULL COMMENT '名称', `resource_type` varchar(255) DEFAULT NULL COMMENT '资源类型,[menu|button]', `permission` varchar(255) DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view', `available` int(255) DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户', PRIMARY KEY (`permission_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='权限表'; -- shiro.`role` definition CREATE TABLE `role` ( `role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `role` varchar(255) DEFAULT NULL COMMENT '角色标识程序中判断使用,如"admin",这个是唯一的:', `description` varchar(255) DEFAULT NULL COMMENT '角色描述,UI界面显示使用', `available` int(255) DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户', PRIMARY KEY (`role_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色表'; -- shiro.role_permission_relation definition CREATE TABLE `role_permission_relation` ( `permission_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`permission_id`,`role_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色权限关系表'; -- shiro.`user` definition CREATE TABLE `user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) DEFAULT NULL COMMENT '登录用户名', `name` varchar(255) DEFAULT NULL COMMENT '名称(昵称或者真实姓名,根据实际情况定义)', `password` varchar(255) DEFAULT NULL COMMENT '密码', `state` int(255) DEFAULT NULL COMMENT '用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `email` varchar(255) DEFAULT NULL COMMENT '邮箱', `expired_date` datetime DEFAULT NULL COMMENT '过期日期', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户信息表'; -- shiro.user_role_relation definition CREATE TABLE `user_role_relation` ( `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`user_id`,`role_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户角色'; -- shiro.`group` definition CREATE TABLE `group` ( `user_group_id` bigint(20) NOT NULL AUTO_INCREMENT, `group_name` varchar(100) NOT NULL, `parent_group_id` bigint(20) DEFAULT NULL, `parent_group_name` varchar(100) DEFAULT NULL, PRIMARY KEY (`user_group_id`), KEY `user_group_user_group_id_IDX` (`user_group_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户组'; -- shiro.user_group_relation definition CREATE TABLE `user_group_relation` ( `user_id` bigint(20) DEFAULT NULL, `group_id` bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- shiro.role_group_relation definition CREATE TABLE `role_group_relation` ( `role_id` bigint(20) DEFAULT NULL, `group_id` bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- shiro.menu definition CREATE TABLE `menu` ( `menu_id` bigint(20) NOT NULL AUTO_INCREMENT, `menu_name` varchar(100) DEFAULT NULL, `url` varchar(100) DEFAULT NULL, `parent_menu_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- shiro.`function` definition CREATE TABLE `function` ( `function_id` bigint(20) NOT NULL AUTO_INCREMENT, `function_name` varchar(100) DEFAULT NULL, `function_code` varchar(100) DEFAULT NULL, `function_url` varchar(100) DEFAULT NULL, `parent_function_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`function_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- shiro.menu_permission_relation definition CREATE TABLE `menu_permission_relation` ( `permission_id` bigint(20) DEFAULT NULL, `menu_id` bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- shiro.function_permission_relation definition CREATE TABLE `function_permission_relation` ( `permission_id` bigint(20) DEFAULT NULL, `function_id` bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 目录结构 ![image-20200616154225112](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/worker/20200616154226.png) > 这里使用了mbg自动生成model、dao、mapper。 #### 配置realm 添加一个MyShiroRealm类,并继承AuthorizingRealm,并且需要实现两个方法。 - doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。 - doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。 ```java package com.felton.springboot.shiro.config; import com.felton.springboot.shiro.entity.dto.UserDTO; import com.felton.springboot.shiro.service.GroupService; import com.felton.springboot.shiro.service.PermissionService; import com.felton.springboot.shiro.service.RoleService; import com.felton.springboot.shiro.service.UserService; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; 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.springframework.beans.factory.annotation.Autowired; /** * 类 名:com.felton.springboot.shiro.config.MyshiroRealm * 类描述:todo * 创建人:liurui * 创建时间:2020/6/10 11:23 * 修改人: * 修改时间: * 修改备注: * * @author liurui * @version 1.0 */ public class MyShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; @Autowired private GroupService groupService; /** * 权限信息 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); UserDTO user = (UserDTO) principalCollection.getPrimaryPrincipal(); // 2023/11/10 角色权限 /* List roleList = roleService.getRoleListByUserName(user.getUserName()); List permissionList = null; if (roleList.size() > 0) { //添加角色 for (Role role : roleList) { authorizationInfo.addRole(role.getRole()); permissionList = permissionService.getPermissionListByRoleId(role.getRoleId()); for (Permission permission : permissionList) { //添加权限 authorizationInfo.addStringPermission(permission.getPermission()); } } }*/ authorizationInfo.addRoles(user.getRoles()); authorizationInfo.addStringPermissions(user.getPermissions()); // TODO: 2023/11/10 组权限 /*List groupPermissionList = groupService.getGroupPermissionByUserName(user.getUserName()); if (CollectionUtils.isNotEmpty(groupPermissionList)) { }*/ return authorizationInfo; } /** * 身份认证 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("【MyShiroRealm】身份认证"); String userName = (String) authenticationToken.getPrincipal(); UserDTO user = userService.findDTOByUserName(userName); if (user == null) { return null; } if (user.getState() == 0) { // 用户被管理员锁定抛出异常 throw new AuthenticationException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, user.getPassword(), getName() ); return authenticationInfo; } } ``` #### 配置shiro 这里使用的是spring boot,所以抛弃传统的xml配置,改为编写配置类`ShiroConfig`。在项目启动的时候,实现方法会给注入到spring容器中。 ```java package com.felton.springboot.shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * 类 名:com.felton.springboot.shiro.config.ShiroConfig * 类描述:todo * 创建人:liurui * 创建时间:2020/6/10 11:24 * 修改人: * 修改时间: * 修改备注: * * @author liurui * @version 1.0 */ @Configuration public class ShiroConfig { /** * 将自己的验证方式加入容器 * @return */ @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } /** * 权限管理,配置主要是realm的管理认证 * @return */ @Bean DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm()); return manager; } /** * 凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 加密方式 hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 加密两次 hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); Map filterMap = new HashMap<>(); // 登出 filterMap.put("/logout", "logout"); // 对所有用户认证 filterMap.put("/**", "authc"); filterMap.put("/swagger**/**", "anon"); filterMap.put("/webjars/**", "anon"); filterMap.put("/v2/**", "anon"); filterMap.put("/group/**", "anon"); // 登录 bean.setLoginUrl("/login"); // 首页 bean.setSuccessUrl("/index"); // 未授权页面,认证不通过跳转 bean.setUnauthorizedUrl("/403"); bean.setFilterChainDefinitionMap(filterMap); return bean; } /** * 开启shiro aop 注解支持 * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { System.out.println("错误"); SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("UnauthorizedException", "/403"); resolver.setExceptionMappings(mappings); resolver.setDefaultErrorView("error"); resolver.setExceptionAttribute("exception"); return resolver; } } ``` #### service层的简单使用 ```java package com.felton.springboot.shiro.service.impl; import com.felton.springboot.shiro.model.LoginResult; import com.felton.springboot.shiro.service.LoginService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; /** * 类 名:com.felton.springboot.shiro.service.impl.LoginServiceImpl * 类描述:todo * 创建人:liurui * 创建时间:2020/6/10 11:08 * 修改人: * 修改时间: * 修改备注: * * @author liurui * @version 1.0 */ @Service public class LoginServiceImpl implements LoginService { @Override public LoginResult login(String userName, String password) { LoginResult result = new LoginResult(); if (userName == null || userName.isEmpty()) { result.setLogin(false); result.setResult("用户名为空"); return result; } String msg = ""; UsernamePasswordToken token = new UsernamePasswordToken(userName, password); try { Subject currentUser = SecurityUtils.getSubject(); currentUser.login(token); Session session = currentUser.getSession(); // 设置会话session session.setAttribute("userName", userName); result.setLogin(true); return result; } catch (Exception e) { e.printStackTrace(); } result.setLogin(false); result.setResult(""); return result; } @Override public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } } ``` 大致的代码都在这里,如果需要完整的项目地址可以到[这里](https://gitee.com/teaegg/spring-boot-shiro) 代码量也不算很多,但是要真正的理解shiro的执行过程还需要对其源码进行分析。简单的走了一下代码的执行过程。 用户的每一次请求都会执行自定义的`AuthorizingRealm`类,如果没有session记录的会执行身份认证`doGetAuthenticationInfo`,如果已经登录并且有session会话记录,则直接执行权限认证`doGetAuthorizationInfo` #### 登录过程 ![image-20200616164215983](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/worker/20200616164216.png) 验证的执行过程 ```mermaid graph TD A[开始] -->|提交信息| B[创建 Subject 对象] B -->|收集用户身份信息| C[创建 AuthenticationToken] C -->|提交 AuthenticationToken| D[SecurityManager 的认证过程] D -->|Authenticator 的认证过程| E[Realm 的参与] E -->|认证结果| F[结束] F -->|成功| G[执行操作] F -->|失败| H[提示错误] ``` #### 权限判断 如果在数据库中匹配到指定的权限,返回true值,由于每次访问接口都会触发`doGetAuthorizationInfo`方法,从而会经常性的查找数据库,如果用户数量大,查找权限多,会导致接口响应慢,这里可以改造使用redis缓存数据库来缓存权限信息。 ![image-20200616164822581](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/worker/20200616164823.png) #### 无权限 如果用户无权限,`isPermitted`会返回false,`ModularRealmAuthorizer`中会抛出无权限异常`UnauthorizedException`,而在ShiroConfig.java中已配置`UnauthorizedException`的异常捕获,并重定向到`/403`接口中。 ![image-20200616165418692](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/worker/20200616165419.png) ### 2023年11月12日 如果需要进行数据权限过滤,而且不影响过去业务代码的情况下,该如何进行?我们可以使用拦截器进行嵌套查询,对已完成的 dao 方法进行封装。使用拦截器进行已有的 SQL 进行封装的前提是需要返回的数据库字段命名需要规范,并且只能对查询语句进行。为了兼容不需要数据权限的 SQL,需要手动打上注解 @DataPermission。 如下代码 ```java /** * @author liurui * @desc 分页拦截器 * @since */ @Component @Slf4j @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DataPermissionInterceptor implements Interceptor { @SuppressWarnings("unchecked") @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); try { if (SqlCommandType.SELECT.equals(sqlCommandType)) { //获取查询注解标志 DataPermission annotation = null; String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1); Class> aClass = Class.forName(className); Method[] declaredMethods = aClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { ReflectionUtils.makeAccessible(declaredMethod); //方法名相同,并且注解是SelectAt if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DataPermission.class)) { annotation = declaredMethod.getAnnotation(DataPermission.class); } } // 如果注解存在并且注解为true(默认为true) 则为mysql语句增加删除标志 if (annotation != null && annotation.enable()) { BoundSql boundSql = statementHandler.getBoundSql(); //获取到原始sql语句 String sql = boundSql.getSql(); StringBuilder stringBuilder = new StringBuilder(sql); // TODO: 2023/11/12 处理数据权限的操作 //通过反射修改sql语句 Field field = boundSql.getClass().getDeclaredField("sql"); ReflectionUtils.makeAccessible(field); field.set(boundSql, stringBuilder.toString()); } } } catch (Exception e) { log.error("DataPermissionInterceptor, {}", e); } return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { throw new UnsupportedOperationException(); } } ``` ### FAQ #### shiro的注解使用 推荐一篇文章,里边说了很明白,[地址](http://www.voidcn.com/article/p-wdqlavod-brz.html) 主要用到的是@RequiresPermissions,注解中的值分为三种形式 - 普通形式 value只是一个普通的字符串,例如 @RequiresPermissions("action") - 多层形式 value中的字符串,使用冒号`:`来分割字符串的内容,例如:@RequiresPermissions("user:add") 一般第一个字符串是操作对象的权限领域,第二的是操作的权限类型 - 多权限多层心事 value由多个多层形式的字符串组成,例如:@RequiresPermissions("user:view","user:add) 打赏: 微信, 支付宝 标签: java, shiro, 安全控制, 权限, RABC 本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
阔以。