之前使用 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";
- }
您可以选择一种方式赞助本站
支付宝扫一扫赞助
微信钱包扫描赞助
赏