SpringBoot整合Shiro权限 SpringBoot中整合Shiro实现权限管理的代码实例
Asurplus、 人气:0之前在 SSM 项目中使用过 shiro,发现 shiro 的权限管理做的真不错,但是在 SSM 项目中的配置太繁杂了,于是这次在 SpringBoot 中使用了 shiro,下面一起看看吧
一、简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
三个核心组件:
1、Subject
即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。
2、SecurityManager
它是Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。
3、Realm
Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个 Realm,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。
二、整合 shiro
1、引入 maven 依赖
<!-- web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Shiro 权限管理 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.4</version> </dependency> <!-- 为了能够在 html 中使用 shiro 的标签引入 --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
我使用的 SpringBoot 版本是 2.3.1,其它依赖自己看着引入吧
2、创建 shiro 配置文件
关于 shiro 的配置信息,我们都放在 ShiroConfig.java 文件中
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; 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.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; /** * shiro配置类 */ @Configuration public class ShiroConfig { /** * 注入这个是是为了在thymeleaf中使用shiro的自定义tag。 */ @Bean(name = "shiroDialect") public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * 地址过滤器 * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 设置securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 设置登录url shiroFilterFactoryBean.setLoginUrl("/login"); // 设置主页url shiroFilterFactoryBean.setSuccessUrl("/"); // 设置未授权的url shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 开放登录接口 filterChainDefinitionMap.put("/doLogin", "anon"); // 开放静态资源文件 filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/layui/**", "anon"); // 其余url全部拦截,必须放在最后 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 自定义安全管理策略 */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); /** 设置自定义的relam */ securityManager.setRealm(loginRelam()); return securityManager; } /** * 登录验证 */ @Bean public LoginRelam loginRelam() { return new LoginRelam(); } /** * 以下是为了能够使用@RequiresPermission()等标签 */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } }
上面开放静态资源文件,其它博客说的是 **filterChainDefinitionMap.put("/static/**", "anon");** ,但我发现,我们在 html 文件中引入静态文件时,请求路径根本没有经过 static,thymeleaf 自动默认配置 **static/** 下面就是静态资源文件,所以,我们开放静态资源文件需要指定响应的目录路径
2、登录验证管理
关于登录验证的一些逻辑,以及赋权等操作,我们都放在 LoginRelam.java 文件中
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zyxx.sbm.entity.UserInfo; import com.zyxx.sbm.service.RolePermissionService; import com.zyxx.sbm.service.UserInfoService; import com.zyxx.sbm.service.UserRoleService; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; 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.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; /** * 登录授权 */ public class LoginRelam extends AuthorizingRealm { @Autowired private UserInfoService userInfoService; @Autowired private UserRoleService userRoleService; @Autowired private RolePermissionService rolePermissionService; /** * 身份认证 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取基于用户名和密码的令牌:实际上这个authcToken是从LoginController里面currentUser.login(token)传过来的 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //根据用户名查找到用户信息 QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("account", token.getUsername()); UserInfo userInfo = userInfoService.getOne(queryWrapper); // 没找到帐号 if (null == userInfo) { throw new UnknownAccountException(); } // 校验用户状态 if ("1".equals(userInfo.getStatus())) { throw new DisabledAccountException(); } // 认证缓存信息 return new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getAccount()), getName()); } /** * 角色授权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { UserInfo authorizingUser = (UserInfo) principalCollection.getPrimaryPrincipal(); if (null != authorizingUser) { //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //获得用户角色列表 Set<String> roleSigns = userRoleService.listUserRoleByUserId(authorizingUser.getId()); simpleAuthorizationInfo.addRoles(roleSigns); //获得权限列表 Set<String> permissionSigns = rolePermissionService.listRolePermissionByUserId(authorizingUser.getId()); simpleAuthorizationInfo.addStringPermissions(permissionSigns); return simpleAuthorizationInfo; } return null; } /** * 自定义加密规则 * * @param credentialsMatcher */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { // 自定义认证加密方式 CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher(); // 设置自定义认证加密方式 super.setCredentialsMatcher(customCredentialsMatcher); } }
以上就是登录时,需要指明 shiro 对用户的一些验证、授权等操作,还有自定义密码验证规则,在第3步会讲到,获取角色列表,权限列表,需要获取到角色与权限的标识,每一个角色,每一个权限都有唯一的标识,装入 Set 中
3、自定义密码验证规则
密码的验证规则,我们放在了 CustomCredentialsMatcher.java 文件中
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import org.apache.shiro.crypto.hash.SimpleHash; /** * @ClassName CustomCredentialsMatcher * 自定义密码加密规则 * @Author Lizhou * @Date 2020-07-10 16:24:24 **/ public class CustomCredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; //加密类型,密码,盐值,迭代次数 Object tokenCredentials = new SimpleHash("md5", token.getPassword(), token.getUsername(), 6).toHex(); // 数据库存储密码 Object accountCredentials = getCredentials(info); // 将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false return equals(tokenCredentials, accountCredentials); } }
我们采用的密码加密方式为 MD5 加密,加密 6 次,使用登录账户作为加密密码的盐进行加密
4、密码加密工具
上面我们自定义了密码加密规则,我们创建一个密码加密的工具类 PasswordUtils.java 文件
import org.apache.shiro.crypto.hash.Md5Hash; /** * 密码加密的处理工具类 */ public class PasswordUtils { /** * 迭代次数 */ private static final int ITERATIONS = 6; private PasswordUtils() { throw new AssertionError(); } /** * 字符串加密函数MD5实现 * * @param password 密码 * @param loginName 用户名 * @return */ public static String getPassword(String password, String loginName) { return new Md5Hash(password, loginName, ITERATIONS).toString(); } }
三、开始登录
上面,我们已经配置了 shiro 的一系列操作,从登录验证、密码验证规则、用户授权等等,下面我们就开始登录,登录的操作,放在了 LoginController.java 文件中
import com.zyxx.common.consts.SystemConst; import com.zyxx.common.enums.StatusEnums; import com.zyxx.common.kaptcha.KaptchaUtil; import com.zyxx.common.shiro.SingletonLoginUtils; import com.zyxx.common.utils.PasswordUtils; import com.zyxx.common.utils.ResponseResult; import com.zyxx.sbm.entity.UserInfo; import com.zyxx.sbm.service.PermissionInfoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @ClassName LoginController * @Description * @Author Lizhou * @Date 2020-07-02 10:54:54 **/ @Api(tags = "后台管理端--登录") @Controller public class LoginController { @Autowired private PermissionInfoService permissionInfoService; @ApiOperation(value = "请求登录页面", notes = "请求登录页面") @GetMapping("login") public String init() { return "login"; } @ApiOperation(value = "请求主页面", notes = "请求主页面") @GetMapping("/") public String index() { return "index"; } @ApiOperation(value = "登录验证", notes = "登录验证") @ApiImplicitParams({ @ApiImplicitParam(name = "account", value = "账号", required = true), @ApiImplicitParam(name = "password", value = "密码", required = true), @ApiImplicitParam(name = "resCode", value = "验证码", required = true), @ApiImplicitParam(name = "rememberMe", value = "记住登录", required = true) }) @PostMapping("doLogin") @ResponseBody public ResponseResult doLogin(String account, String password, String resCode, Boolean rememberMe, HttpServletRequest request, HttpServletResponse response) throws Exception { // 验证码 if (!KaptchaUtil.validate(resCode, request)) { return ResponseResult.getInstance().error(StatusEnums.KAPTCH_ERROR); } // 验证帐号和密码 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(account, password); // 记住登录状态 token.setRememberMe(rememberMe); try { // 执行登录 subject.login(token); // 将用户保存到session中 UserInfo userInfo = (UserInfo) subject.getPrincipal(); request.getSession().setAttribute(SystemConst.SYSTEM_USER_SESSION, userInfo); return ResponseResult.getInstance().success(); } catch (UnknownAccountException e) { return ResponseResult.getInstance().error("账户不存在"); } catch (DisabledAccountException e) { return ResponseResult.getInstance().error("账户已被冻结"); } catch (IncorrectCredentialsException e) { return ResponseResult.getInstance().error("密码不正确"); } catch (ExcessiveAttemptsException e) { return ResponseResult.getInstance().error("密码连续输入错误超过5次,锁定半小时"); } catch (RuntimeException e) { return ResponseResult.getInstance().error("未知错误"); } } @ApiOperation(value = "登录成功,跳转主页面", notes = "登录成功,跳转主页面") @PostMapping("success") public String success() { return "redirect:/"; } @ApiOperation(value = "初始化菜单数据", notes = "初始化菜单数据") @GetMapping("initMenu") @ResponseBody public String initMenu() { return permissionInfoService.initMenu(); } @ApiOperation(value = "退出登录", notes = "退出登录") @GetMapping(value = "loginOut") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "login"; } }
当执行 subject.login(token); 时,就会进入我们在 第二步中第二条登录验证中,对用户密码、状态进行检查,对用户授权等操作,登录的密码,一定是通过密码加密工具得到的,不然验证不通过
四、页面权限控制
我们本次使用的是 thymeleaf 模板引擎,我们需要在 html 文件中加入以下内容
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
引入了 thymeleaf 的依赖,以及 shiro 的依赖,这样我们就能在 html 文件中使用 thymeleaf、shiro 的标签了
例如:
1、判断当前用户有无此权限,通过权限标识
<button class="layui-btn" shiro:hasPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
2、与上面相反,判断当前用户无此权限,通过权限标识,没有时验证通过
<button class="layui-btn" shiro:lacksPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
3、判断当前用户有无以下全部权限,通过权限标识
<button class="layui-btn" shiro:hasAllPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
4、判断当前用户有无以下任一权限,通过权限标识
<button class="layui-btn" shiro:hasAnyPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
5、判断当前用户有无此角色,通过角色标识
<a shiro:hasRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
6、与上面相反,判断当前用户无此角色,通过角色标识,没有时验证通过
<a shiro:lacksRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
7、判断当前用户有无以下全部角色,通过角色标识
<a shiro:hasAllRoles="admin,role1,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
8、判断当前用户有无以下任一角色,通过角色标识
<a shiro:hasAnyRoles="admin,role1,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
加载全部内容