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

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

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

 一、数据表设计

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

1. 用户表

 

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

 

 

 

 

2. 角色表

 

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

 

 

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

 

3. 权限表

 

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

 

 

 

 

  4. 用户和角色关联表

 

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

 

 

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

 

  5. 角色和权限关联表

 

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

 

 

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

 

二、依赖版本

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

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";
}