Spring Boot集成Shrio实现权限管理
Spring Boot集成Shrio实现权限管理
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。相比于Spring Security,功能没有那么强大,但现实开发中,我们也不需要那么多的功能。
shiro中三个核心组件:Subject, SecurityManager 和 Realms
- Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
- SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
- Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。用户一般会自定义Ream,集成AuthorizingRealm。
对于shiro的基本概念介绍如上,本文主要讲Spring Boot如何集成shiro,如何使用。另外该项目使用mybatis-plus操纵数据库,如果有朋友不知道mybatis-plus如何使用,点击链接https://mp.baomidou.com/ 查看如何而是用。项目中pom.xml文件内容如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com</groupId> <artifactId>springboot-shrio</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-shrio</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!--json--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> <dependency> <groupId>com.ibeetl</groupId> <artifactId>beetl</artifactId> <version>3.1.3.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
在项目中有两个至关重要类需要我们自定义实现,一个是shiroConfig类,一个是CustonRealm类。
ShiroConfig类:
顾名思义就是对shiro的一些配置,相对于之前的xml配置。包括:过滤的文件和权限,密码加密的算法,其用注解等相关功能。
package com.shiro.config; import com.shiro.realm.CustomRealm; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @Author IT咸鱼 * @Date 2020/04/26 */ @Configuration public class ShiroConfig { private Logger logger = LoggerFactory.getLogger(this.getClass()); //不加这个注解不生效,具体不详 @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; } //将自己的验证方式加入容器 @Bean public CustomRealm myShiroRealm() { CustomRealm customRealm = new CustomRealm(); return customRealm; } //权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { logger.info("SecurityManager注册完成"); DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { logger.info("设置对应的过滤条件和跳转条件"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String,String> map = new HashMap<String,String>(); // 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录 map.put("/css/**", "anon"); map.put("/fonts/**", "anon"); map.put("/img/**", "anon"); map.put("/js/**", "anon"); map.put("/html/**", "anon"); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 map.put("/logout", "logout"); //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> map.put("/**", "authc"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
CustomRealm类:
自定义的CustomRealm继承AuthorizingRealm。并且重写父类中的doGetAuthorizationInfo(权限相关)、doGetAuthenticationInfo(身份认证)这两个方法。
package com.shiro.realm; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.shiro.entity.*; import com.shiro.service.ITPermissionService; import com.shiro.service.ITRoleService; import com.shiro.service.ITUserService; import com.shiro.service.LoginService; import org.apache.catalina.authenticator.jaspic.SimpleAuthConfigProvider; import org.apache.shiro.authc.*; 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.beans.factory.annotation.Autowired; import java.util.List; public class CustomRealm extends AuthorizingRealm { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired LoginService loginServiceImpl; @Autowired ITUserService tUserServiceImpl; @Autowired ITRoleService tRoleServiceImpl; @Autowired ITPermissionService tPermissionServiceImpl; /** * 授权 * @param principalCollection * @return * @throws AuthenticationException */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) throws AuthenticationException { logger.info("CustomRealm.doGetAuthorizationInfo,PrincipalCollection={}", principalCollection); TUser tUser = (TUser)principalCollection.getPrimaryPrincipal(); //添加角色和权限 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); List<TRole> roles = tRoleServiceImpl.getRoleByUserId(tUser.getId()); for (TRole tRole : roles){ authorizationInfo.addRole(tRole.getRoleCode()); List<TPermission> permissions = tPermissionServiceImpl.getPermissionsByRoleId(tRole.getId()); for (TPermission tPermission : permissions){ authorizationInfo.addStringPermission(tPermission.getPermissionCode()); } } return authorizationInfo; } /** * 用户调用登录接口时调用该方法,校验用户合法性 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("CustomRealm.doGetAuthenticationInfo,AuthenticationToken={}", authenticationToken); if (authenticationToken.getPrincipal() == null){ return null; } String userName = authenticationToken.getPrincipal().toString(); QueryWrapper<TUser> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda().eq(TUser::getUserName, userName); TUser tUser = tUserServiceImpl.getOne(queryWrapper); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); if (tUser == null){ throw new UnknownAccountException(); } if (tUser.getStatus() == 0){ throw new LockedAccountException(); } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(tUser, tUser.getPassword().toString(), getName()); return simpleAuthenticationInfo; } }
创建LoginController类,使用postman测试登录接口,获取权限
package com.shiro.controller; import com.shiro.dto.LoginDto; import com.shiro.entity.TUser; import com.shiro.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.cache.Cache; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.Serializable; import java.util.Deque; @RestController public class LoginController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @RequestMapping("/login") public String login(LoginDto loginDto) { logger.info("/login, LoginDto={}", loginDto); //添加用户认证信息 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken( loginDto.getUserName(), loginDto.getPassword() ); try { //进行验证,这里可以捕获异常,然后返回对应信息 subject.login(usernamePasswordToken); } catch (AuthenticationException e) { e.printStackTrace(); return "账号或密码错误!"; } catch (AuthorizationException e) { e.printStackTrace(); return "没有权限"; } return "login success"; } @RequestMapping("/logout") public String logout(){ logger.info("/logout"); Subject subject = SecurityUtils.getSubject(); if(null!=subject){ String username = ((TUser) SecurityUtils.getSubject().getPrincipal()).getUserName(); logger.info("username={}", username); } return "logout success"; } }
使用postman访问/login接口
登录成功后,根据登录成功后的用户权限去操作接口,demo中只有admin和common角色,admin可以增加、删除、更新、读取,common用户只能读取,拿用户管理类TUserController作为例子讲解
package com.shiro.controller; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.shiro.common.ResultHandler; import com.shiro.entity.TUser; import com.shiro.service.ITUserService; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; /** * <p> * 前端控制器 * </p> * * @author xieya * @since 2020-04-28 */ @RestController @RequestMapping("/user") public class TUserController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private ITUserService tUserServiceImpl; @RequiresRoles("admin")//指定需要有admin角色 @RequiresPermissions({"create","update"})//需要有create、update权限 @PostMapping("/save-or-update") public String saveOrUpdate(@RequestBody TUser tUser){ logger.info("/save-or-update, TUser={}", tUser); JSONObject jsonObject = null; try { jsonObject = new JSONObject(); if (tUser == null){ return ResultHandler.handler(jsonObject, "-1001", "param null"); } tUserServiceImpl.saveOrUpdate(tUser); ResultHandler.handler(jsonObject, "0", "Success"); } catch (Exception e) { logger.info("System Exception,e={}", e); return ResultHandler.handler(jsonObject, "-9001", "System Exception"); } return jsonObject.toString(); } @RequiresRoles("admin") @RequiresPermissions("delete") @GetMapping("/delete") public String delete(Long id){ logger.info("/delete, id={}", id); JSONObject jsonObject = null; try { jsonObject = new JSONObject(); if (id == null){ return ResultHandler.handler(jsonObject, "-1001", "param null"); } tUserServiceImpl.removeById(id); ResultHandler.handler(jsonObject, "0", "Success"); } catch (Exception e) { logger.info("System Exception,e={}", e); return ResultHandler.handler(jsonObject, "-9001", "System Exception"); } return jsonObject.toString(); } @PostMapping("/retrieve") public String retrieve(@RequestBody TUser tUser){ logger.info("/retrieve, TUser={}", tUser); JSONObject jsonObject = null; try { jsonObject = new JSONObject(); if (tUser == null){ return ResultHandler.handler(jsonObject, "-1001", "param null"); } QueryWrapper<TUser> queryWrapper = new QueryWrapper<>(); if (tUser.getId() != null){ queryWrapper.lambda().eq(TUser::getId, tUser.getId()); } if (!StringUtils.isEmpty(tUser.getUserName())){ queryWrapper.lambda().eq(TUser::getUserName, tUser.getUserName()); } List<TUser> list = tUserServiceImpl.list(queryWrapper); ResultHandler.handler(jsonObject, "0", "Success", list); } catch (Exception e) { logger.info("System Exception,e={}", e); return ResultHandler.handler(jsonObject, "-9001", "System Exception"); } return jsonObject.toString(); } }
登录之后使用postman去访问“/user/save-or-update”接口
如果在没有登录的情况下访问该接口,就会出现如下错误,在没有登录的请况下,shiro会自动的将接口访问重置到login接口login3
一个简单的小项目,希望能帮上大家
技术交流QQ群:579949017
或者添加个人微信:xieya0126 加入微信交流群