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

avatar 2019年10月14日13:13:52 6 3735 views
博主分享免费Java教学视频,B站账号:Java刘哥

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

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

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

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

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

3. 自定义 URL 拦截器:URLPathMatchingFilter

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

现在在 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.  
  3. import lombok.Data;
  4. import org.springframework.boot.context.properties.ConfigurationProperties;
  5. import org.springframework.context.annotation.Configuration;
  6.  
  7. import java.util.ArrayList;
  8. import java.util.List;
  9.  
  10. /**
  11.  * @author liuyanzhao
  12.  */
  13. @Data
  14. @Configuration
  15. @ConfigurationProperties(prefix = "ignored")
  16. public class IgnoredUrlsProperties {
  17.  
  18.     private List<String> urls = new ArrayList<>();
  19. }

通过注入这个类就能获取 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

发表评论

avatar 登录者:匿名
匿名评论,评论回复后会有邮件通知

  

已通过评论:0   待审核评论数:0