SpringBoot2.x配置Shiro实现权限管理,根据URL鉴权
之前使用 Shiro 鉴权的时候,一直用的是注解,如 @RequiresPermissions() 和 @RequiresRoles(),这种方法不利于维护和动态修改,代码侵入性强。所以,为了解决这个问题,通常都会采用URL鉴权,当写一个拦截器,获取请求的URL,然后查询当前登录用户的权限列表,判断请求的URL是否在权限列表的URL内,如果在则放行,否则拦截。
之前介绍了SpringSecurity权限管理,根据请求URL鉴权 ,本文就介绍一下 Shiro 的实现。
一、数据表设计
这里截图贴出几张表核心字段和部分数据
1. 用户表
2. 角色表
3. 权限表
4. 用户和角色关联表
5. 角色和权限关联表
二、依赖版本
springboot 版本 2.1.7.RELEASE
添加 shiro 依赖
-
<dependency>
-
<groupId>org.apache.shiro</groupId>
-
<artifactId>shiro-spring</artifactId>
-
<version>1.4.0</version>
-
</dependency>
三、Shiro 相关配置
1.自定义 Realm
package com.liuyanzhao.sens.config.shiro; import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Validator; import com.liuyanzhao.sens.entity.Permission; import com.liuyanzhao.sens.entity.Role; import com.liuyanzhao.sens.entity.User; import com.liuyanzhao.sens.service.PermissionService; import com.liuyanzhao.sens.service.RoleService; import com.liuyanzhao.sens.service.UserService; import com.liuyanzhao.sens.utils.LocaleMessageUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; 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.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 默认的realm * * @author 言曌 * @date 2018/9/1 上午10:47 */ @Slf4j public class MyRealm extends AuthorizingRealm { @Autowired @Lazy private UserService userService; @Autowired @Lazy private RoleService roleService; @Autowired @Lazy private PermissionService permissionService; /** * 认证信息(身份验证) Authentication 是用来验证用户身份 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { User user = userService.findByUserName(account); if (user == null) { return null; } //封装authenticationInfo,准备验证密码 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, // 用户名 user.getUserPass(), // 密码 ByteSource.Util.bytes("sens"), // 盐 getName() // realm name return authenticationInfo; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principals.getPrimaryPrincipal(); List<Role> roles = roleService.listRolesByUserId(user.getId()); for (Role role : roles) { authorizationInfo.addRole(role.getRole()); List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId()); for (Permission p : permissions) { authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } }
注意:我们加密方式采用加盐(固定字符串 sens),md5十次
用户注册或添加用户或修改密码的时候
需要对用户密码加盐 sens,然后md5加密十次
可以使用 shiro 的 new Md5Hash(pwd, salt, i) 实现
如示例
user.setUserPass(new Md5Hash(password, "sens", 10).toString());
关于 permissionService 和 roleService 这里应该不用贴吧,大家应该能看懂吧
2. ShiroConfig
package com.liuyanzhao.sens.config.shiro; import com.liuyanzhao.sens.config.properties.IgnoredUrlsProperties; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; 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 javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * @author 言曌 * @date 2018/8/20 上午6:19 */ @Configuration public class ShiroConfig { @Bean IgnoredUrlsProperties getIgnoredUrlsProperties() { return new IgnoredUrlsProperties(); } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //自定义拦截器 Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>(); //访问权限配置 filtersMap.put("requestURL", getURLPathMatchingFilter()); shiroFilterFactoryBean.setFilters(filtersMap); //拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不会被拦截的链接 顺序判断 List<String> urls = getIgnoredUrlsProperties().getUrls(); for (String url : urls) { filterChainDefinitionMap.put(url, "anon"); } filterChainDefinitionMap.put("/admin", "requestURL"); filterChainDefinitionMap.put("/admin/**", "requestURL"); filterChainDefinitionMap.put("/**", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面 shiroFilterFactoryBean.setLoginUrl("/admin/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm()); return securityManager; } /** * 需要密码登录的realm * * @return MyShiroRealm */ @Bean public MyRealm myRealm() { MyRealm myRealm = new MyRealm(); myRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myRealm; } /** * 凭证匹配器 * <p> * 加密算法:md5加盐加密10次 * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列的次数,md5("") hashedCredentialsMatcher.setHashIterations(10); return hashedCredentialsMatcher; } /** * 访问 权限 拦截器 * * @return */ public URLPathMatchingFilter getURLPathMatchingFilter() { return new URLPathMatchingFilter(); } }
之前我们都是用,authc 是 shiro 内部的,目前不满足我们的需求
filterChainDefinitionMap.put("/admin/**", "authc");
我们需要自己写一个根据 URL 过滤的拦截器,即 URLPathMatchingFilter 类
3. 自定义 URL 拦截器:URLPathMatchingFilter
package com.liuyanzhao.sens.config.shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.PathMatchingFilter; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.util.Set; /** * URL拦截器 * @author 言曌 * @date 2019-10-12 17:56 */ public class URLPathMatchingFilter extends PathMatchingFilter { @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { //请求的url String requestURL = getPathWithinApplication(request); System.out.println("请求的url :" + requestURL); Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { // 如果没有登录, 跳到登录页面 WebUtils.issueRedirect(request, response, "/admin/login"); return false; } //从session里读取当前用户的权限URL列表 Set<String> urls = (Set<String>) subject.getSession().getAttribute("permissionUrls"); if (urls.contains(requestURL)) { return true; } //没有权限,跳到403页面 WebUtils.issueRedirect(request, response, "/403"); return false; } }
现在在 URL 拦截器里,从 session 里查询当前登录用户的权限URL列表,然后判断请求的URL是否在那个URL列表里就行。
(说明一下:登录成功的时候,我们会查询当前登录用户的权限列表,从里面获取URL列表,然后放到 Session 里。)
4. 将匿名访问的URL写到 application.yml 中
这里我们拦截 /admin/** 的页面
但是想要放行一些特殊的,如 /admin/login,/admin/register 这些是登录页面
之前我们都是直接写
filterChainDefinitionMap.put("/admin/login", "anno"); filterChainDefinitionMap.put("/admin/register", "anno");
但是一旦多起来比较麻烦,我们希望写在配置文件里
如下 application.yml
# 忽略鉴权url,即设置为anon的url ignored: urls: - /admin/login - /admin/getLogin - /admin/register - /admin/getRegister - /admin/forget - /admin/getForget
然后创建一个类 IgnoredUrlsProperties
package com.liuyanzhao.sens.config.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** * @author liuyanzhao */ @Data @Configuration @ConfigurationProperties(prefix = "ignored") public class IgnoredUrlsProperties { private List<String> urls = new ArrayList<>(); }
通过注入这个类就能获取 urls
但是在 ShiroConfig 里无法直接注入,如
/** * 无法注入,ignoredUrlsProperties为null * 需要改成 @Bean */ @Autowired private IgnoredUrlsProperties ignoredUrlsProperties;
这样是不行的
需要改成 @Bean 这种,手动 new 一个
@Bean IgnoredUrlsProperties getIgnoredUrlsProperties() { return new IgnoredUrlsProperties(); }
至此 shiro 的相关配置就结束了
下面介绍一下登录和登出
四、登录和登出
1.登录
/** * 验证登录信息 * * @param account 用户名 * @param password password 密码 * @return JsonResult JsonResult */ @PostMapping(value = "/getLogin") @ResponseBody public JsonResult getLogin(String account, String password) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(account, password); try { subject.login(token); if (subject.isAuthenticated()) { User user = (User) subject.getPrincipal(); // 将用户的权限URL列表放到 session 中 Set<String> permissionUrls = permissionService.findPermissionUrlsByUserId(user.getId()); subject.getSession().setAttribute("permissionUrls", permissionUrls); return new JsonResult(200, "登录成功"); } } catch (UnknownAccountException e) { log.info("UnknownAccountException -- > 账号不存在:"); return new JsonResult(500, "账号不存在"); } catch (IncorrectCredentialsException e) { return new JsonResult(500, "密码错误"); } } catch (LockedAccountException e) { log.info("LockedAccountException -- > 账号被锁定"); return new JsonResult(500, "账号被锁定"); } catch (Exception e) { log.info(e.getMessage()); } return new JsonResult(500, "服务器内部错误"); }
2.登出
/** * 退出登录 * * @return 重定向到/admin/login */ @GetMapping(value = "/logOut") public String logOut() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/admin/login"; }