SpringBoot2.x配置Shiro实现权限管理,根据URL鉴权

avatar 2019年10月14日13:13:52 评论 101 views

之前使用 Shiro 鉴权的时候,一直用的是注解,如 @RequiresPermissions() 和 @RequiresRoles(),这种方法不利于维护和动态修改,代码侵入性强。所以,为了解决这个问题,通常都会采用URL鉴权,当写一个拦截器,获取请求的URL,然后查询当前登录用户的权限列表,判断请求的URL是否在权限列表的URL内,如果在则放行,否则拦截。

之前介绍了SpringSecurity权限管理,根据请求URL鉴权 ,本文就介绍一下 Shiro 的实现。

 一、数据表设计

这里截图贴出几张表核心字段和部分数据

1. 用户表

   

2. 角色表

3. 权限表

  4. 用户和角色关联表

  5. 角色和权限关联表

 

二、依赖版本

springboot 版本 2.1.7.RELEASE

添加 shiro 依赖

  1. <dependency>
  2.     <groupId>org.apache.shiro</groupId>
  3.     <artifactId>shiro-spring</artifactId>
  4.     <version>1.4.0</version>
  5. </dependency>

 

三、Shiro 相关配置

1.自定义 Realm

  1. package com.liuyanzhao.sens.config.shiro;
  2. import cn.hutool.core.date.DateUnit;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.lang.Validator;
  5. import com.liuyanzhao.sens.entity.Permission;
  6. import com.liuyanzhao.sens.entity.Role;
  7. import com.liuyanzhao.sens.entity.User;
  8. import com.liuyanzhao.sens.service.PermissionService;
  9. import com.liuyanzhao.sens.service.RoleService;
  10. import com.liuyanzhao.sens.service.UserService;
  11. import com.liuyanzhao.sens.utils.LocaleMessageUtil;
  12. import lombok.extern.slf4j.Slf4j;
  13. import org.apache.commons.lang3.StringUtils;
  14. import org.apache.shiro.authc.*;
  15. import org.apache.shiro.authz.AuthorizationInfo;
  16. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  17. import org.apache.shiro.realm.AuthorizingRealm;
  18. import org.apache.shiro.subject.PrincipalCollection;
  19. import org.apache.shiro.util.ByteSource;
  20. import org.springframework.beans.factory.annotation.Autowired;
  21. import org.springframework.context.annotation.Lazy;
  22. import java.util.Date;
  23. import java.util.List;
  24. import java.util.Objects;
  25. import java.util.Set;
  26. import java.util.stream.Collectors;
  27. /**
  28.  * 默认的realm
  29.  *
  30.  * @author 言曌
  31.  * @date 2018/9/1 上午10:47
  32.  */
  33. @Slf4j
  34. public class MyRealm extends AuthorizingRealm {
  35.     @Autowired
  36.     @Lazy
  37.     private UserService userService;
  38.     @Autowired
  39.     @Lazy
  40.     private RoleService roleService;
  41.     @Autowired
  42.     @Lazy
  43.     private PermissionService permissionService;
  44.     /**
  45.      * 认证信息(身份验证) Authentication 是用来验证用户身份
  46.      */
  47.     @Override
  48.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  49.         User user = userService.findByUserName(account);
  50.         if (user == null) {
  51.             return null;  
  52.         }
  53.         //封装authenticationInfo,准备验证密码
  54.         SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
  55.                 user, // 用户名
  56.                 user.getUserPass(), // 密码
  57.                 ByteSource.Util.bytes("sens"), // 盐
  58.                 getName() // realm name
  59.         return authenticationInfo;
  60.     }
  61.     @Override
  62.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  63.         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  64.         User user = (User) principals.getPrimaryPrincipal();
  65.         List<Role> roles = roleService.listRolesByUserId(user.getId());
  66.         for (Role role : roles) {
  67.             authorizationInfo.addRole(role.getRole());
  68.             List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
  69.              for (Permission p : permissions) {
    authorizationInfo.addStringPermission(p.getPermission());
    }
  70.         }
  71.         return authorizationInfo;
  72.     }
  73. }

注意:我们加密方式采用加盐(固定字符串 sens),md5十次

用户注册或添加用户或修改密码的时候

需要对用户密码加盐 sens,然后md5加密十次

可以使用 shiro 的 new Md5Hash(pwd, salt, i) 实现

如示例

  1. user.setUserPass(new Md5Hash(password, "sens"10).toString());

关于 permissionService 和 roleService 这里应该不用贴吧,大家应该能看懂吧

 

2. ShiroConfig

  1. package com.liuyanzhao.sens.config.shiro;
  2. import com.liuyanzhao.sens.config.properties.IgnoredUrlsProperties;
  3. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
  4. import org.apache.shiro.mgt.SecurityManager;
  5. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  6. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import javax.servlet.Filter;
  10. import java.util.LinkedHashMap;
  11. import java.util.List;
  12. import java.util.Map;
  13. /**
  14.  * @author 言曌
  15.  * @date 2018/8/20 上午6:19
  16.  */
  17. @Configuration
  18. public class ShiroConfig {
  19.     @Bean
  20.     IgnoredUrlsProperties getIgnoredUrlsProperties() {
  21.         return new IgnoredUrlsProperties();
  22.     }
  23.     @Bean
  24.     public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
  25.         ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  26.         shiroFilterFactoryBean.setSecurityManager(securityManager);
  27.         //自定义拦截器
  28.         Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
  29.         //访问权限配置
  30.         filtersMap.put("requestURL", getURLPathMatchingFilter());
  31.         shiroFilterFactoryBean.setFilters(filtersMap);
  32.         //拦截器.
  33.         Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
  34.         // 配置不会被拦截的链接 顺序判断
  35.         List<String> urls = getIgnoredUrlsProperties().getUrls();
  36.         for (String url : urls) {
  37.             filterChainDefinitionMap.put(url, "anon");
  38.         }
  39.         filterChainDefinitionMap.put("/admin""requestURL");
  40.         filterChainDefinitionMap.put("/admin/**""requestURL");
  41.         filterChainDefinitionMap.put("/**""anon");
  42.         shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  43.         // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
  44.         shiroFilterFactoryBean.setLoginUrl("/admin/login");
  45.         // 登录成功后要跳转的链接
  46.         shiroFilterFactoryBean.setSuccessUrl("/");
  47.         //未授权界面;
  48.         shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  49.         return shiroFilterFactoryBean;
  50.     }
  51.     @Bean
  52.     public SecurityManager securityManager() {
  53.         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  54.         securityManager.setRealm(myRealm());
  55.         return securityManager;
  56.     }
  57.     /**
  58.      * 需要密码登录的realm
  59.      *
  60.      * @return MyShiroRealm
  61.      */
  62.     @Bean
  63.     public MyRealm myRealm() {
  64.         MyRealm myRealm = new MyRealm();
  65.         myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
  66.         return myRealm;
  67.     }
  68.     /**
  69.      * 凭证匹配器
  70.      * <p>
  71.      * 加密算法:md5加盐加密10次
  72.      *
  73.      * @return
  74.      */
  75.     @Bean
  76.     public HashedCredentialsMatcher hashedCredentialsMatcher() {
  77.         HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
  78.         //散列算法:这里使用MD5算法;
  79.         hashedCredentialsMatcher.setHashAlgorithmName("md5");
  80.         //散列的次数,md5("")
  81.         hashedCredentialsMatcher.setHashIterations(10);
  82.         return hashedCredentialsMatcher;
  83.     }
  84.     /**
  85.      * 访问 权限 拦截器
  86.      *
  87.      * @return
  88.      */
  89.     public URLPathMatchingFilter getURLPathMatchingFilter() {
  90.         return new URLPathMatchingFilter();
  91.     }
  92. }

之前我们都是用,authc 是 shiro 内部的,目前不满足我们的需求

  1. filterChainDefinitionMap.put("/admin/**""authc");

我们需要自己写一个根据 URL 过滤的拦截器,即 URLPathMatchingFilter 类

 

3. 自定义 URL 拦截器:URLPathMatchingFilter

  1. package com.liuyanzhao.sens.config.shiro;
  2. import org.apache.shiro.SecurityUtils;
  3. import org.apache.shiro.subject.Subject;
  4. import org.apache.shiro.web.filter.PathMatchingFilter;
  5. import org.apache.shiro.web.util.WebUtils;
  6. import javax.servlet.ServletRequest;
  7. import javax.servlet.ServletResponse;
  8. import java.util.Set;
  9. /**
  10.  * URL拦截器
  11.  * @author 言曌
  12.  * @date 2019-10-12 17:56
  13.  */
  14. public class URLPathMatchingFilter extends PathMatchingFilter {
  15.     @Override
  16.     protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  17.         //请求的url
  18.         String requestURL = getPathWithinApplication(request);
  19.         System.out.println("请求的url :" + requestURL);
  20.         Subject subject = SecurityUtils.getSubject();
  21.         if (!subject.isAuthenticated()) {
  22.             // 如果没有登录, 跳到登录页面
  23.             WebUtils.issueRedirect(request, response, "/admin/login");
  24.             return false;
  25.         }
  26.         //从session里读取当前用户的权限URL列表
  27.         Set<String> urls = (Set<String>) subject.getSession().getAttribute("permissionUrls");
  28.         if (urls.contains(requestURL)) {
  29.             return true;
  30.         }
  31.         //没有权限,跳到403页面
  32.         WebUtils.issueRedirect(request, response, "/403");
  33.         return false;
  34.     }
  35. }

现在在 URL 拦截器里,从 session 里查询当前登录用户的权限URL列表,然后判断请求的URL是否在那个URL列表里就行。

(说明一下:登录成功的时候,我们会查询当前登录用户的权限列表,从里面获取URL列表,然后放到 Session 里。)

 

4. 将匿名访问的URL写到 application.yml 中

这里我们拦截 /admin/** 的页面

但是想要放行一些特殊的,如 /admin/login,/admin/register 这些是登录页面

之前我们都是直接写

  1. filterChainDefinitionMap.put("/admin/login""anno");
  2. filterChainDefinitionMap.put("/admin/register""anno");

但是一旦多起来比较麻烦,我们希望写在配置文件里

如下 application.yml

  1. # 忽略鉴权url,即设置为anon的url
  2. ignored:
  3.   urls:
  4.     - /admin/login
  5.     - /admin/getLogin
  6.     - /admin/register
  7.     - /admin/getRegister
  8.     - /admin/forget
  9.     - /admin/getForget

然后创建一个类 IgnoredUrlsProperties

  1. package com.liuyanzhao.sens.config.properties;
  2. import lombok.Data;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.context.annotation.Configuration;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. /**
  8.  * @author liuyanzhao
  9.  */
  10. @Data
  11. @Configuration
  12. @ConfigurationProperties(prefix = "ignored")
  13. public class IgnoredUrlsProperties {
  14.     private List<String> urls = new ArrayList<>();
  15. }

通过注入这个类就能获取 urls

但是在 ShiroConfig 里无法直接注入,如

  1. /**
  2.  * 无法注入,ignoredUrlsProperties为null
  3.  * 需要改成 @Bean
  4.  */
  5. @Autowired
  6. private IgnoredUrlsProperties ignoredUrlsProperties;

这样是不行的

需要改成 @Bean 这种,手动 new 一个

  1. @Bean
  2. IgnoredUrlsProperties getIgnoredUrlsProperties() {
  3.     return new IgnoredUrlsProperties();
  4. }

 

至此 shiro 的相关配置就结束了

下面介绍一下登录和登出

 

四、登录和登出

1.登录

  1. /**
  2.      * 验证登录信息
  3.      *
  4.      * @param account  用户名
  5.      * @param password password 密码
  6.      * @return JsonResult JsonResult
  7.      */
  8.     @PostMapping(value = "/getLogin")
  9.     @ResponseBody
  10.     public JsonResult getLogin(String account, String password) {
  11.         Subject subject = SecurityUtils.getSubject();
  12.         UsernamePasswordToken token = new UsernamePasswordToken(account, password);
  13.         try {
  14.             subject.login(token);
  15.             if (subject.isAuthenticated()) {
  16.                 User user = (User) subject.getPrincipal();
  17.                 // 将用户的权限URL列表放到 session 中
  18.                 Set<String> permissionUrls = permissionService.findPermissionUrlsByUserId(user.getId());
  19.                 subject.getSession().setAttribute("permissionUrls", permissionUrls);
  20.                 return new JsonResult(200"登录成功");
  21.             }
  22.         } catch (UnknownAccountException e) {
  23.             log.info("UnknownAccountException -- > 账号不存在:");
  24.             return new JsonResult(500"账号不存在");
  25.         } catch (IncorrectCredentialsException e) {
  26.                 return new JsonResult(500"密码错误");
  27.             }
  28.         } catch (LockedAccountException e) {
  29.             log.info("LockedAccountException -- > 账号被锁定");
  30.             return new JsonResult(500"账号被锁定");
  31.         } catch (Exception e) {
  32.             log.info(e.getMessage());
  33.         }
  34.         return new JsonResult(500"服务器内部错误");
  35.     }

 

2.登出

  1. /**
  2.  * 退出登录
  3.  *
  4.  * @return 重定向到/admin/login
  5.  */
  6. @GetMapping(value = "/logOut")
  7. public String logOut() {
  8.     Subject subject = SecurityUtils.getSubject();
  9.     subject.logout();
  10.     return "redirect:/admin/login";
  11. }

 

 

  • 微信
  • 交流学习,有偿服务
  • weinxin
  • 博客/Java交流群
  • 资源分享,问题解决,技术交流。群号:590480292
  • weinxin
avatar

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: