基于 SpringBoot+Shiro+Redis 实现动态权限管理

一、背景

最近学了 Apache Shiro,刚好有个新项目需要完成,所以整理一下如何通过整合 SpringBoot + Shiro + Redis 实现登录认证和动态的权限管理的知识点。
实现过程中主要有两个问题待解决:

  1. 如何实现登录认证?
  2. 如何实现动态的权限管理?

本文主要是对下面这两个问题进行解决。

二、登录认证

1、导包
SpringBoot 相关的包不再赘述,Shiro 所需要的包为:

        <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

        <!-- shiro-ehcache -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>${shiro.version}</version>
        </dependency>

由于需要使用到 Redis 去集成 Shiro 实现缓存,所以引入开源插件 shiro-redis

	    <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>

2、登录
在登录的 Contoller 方法接收前台传入的用户名和密码封装成 UsernamePasswordToken 对象,然后交给 SecurityUtils.getSubject() 的主体,调用 login 登陆方法,调用登录方法后,shiro 会委托 SecurityManager 进行身份认证,最终的认证逻辑实现写在我们自定义的 Realm 中。登录方法实现大致为:

	@GetMapping("/login")
	public ResultHelper login(String userName, String password) {
	     log.info("loginId:{},password:{}", userName, password);
	     // 空值判断
	     if (StringUtils.isBlank(userName) || StringUtils.isBlank(password)) {
	         return ResultHelper.fail("登录账号或密码为空!");
	     }
	     Subject subject = SecurityUtils.getSubject();
	     UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
	     String failMsg = "";
	     try {
	         subject.login(token);
	         Map<String, String> data = MapHelper.ofHashMap("token", subject.getSession().getId());
	         return ResultHelper.loginSuccess(data);
	     } catch (UnknownAccountException e) {
	         failMsg = "用户不存在";
	     } catch (IncorrectCredentialsException e) {
	         failMsg = "密码错误!";
	     } catch (LockedAccountException e) {
	         failMsg = "登录失败,该用户已被冻结";
	     } catch (Exception e) {
	         log.error("系统内部异常!!{}", e);
	         return ResultHelper.internalServerError(e);
	     }
	     return ResultHelper.loginFail(failMsg);
	 }

3、自定义 Realm 实现
为了实现登录认证和授权的方法,我们需要自定义 Realm 继承 AuthorizingRealm,然后重写 doGetAuthenticationInfo 方法实现身份认证和 doGetAuthorizationInfo 方法实现授权。
doGetAuthenticationInfo:认证方法,根据前台传入的用户名和其他条件从数据库中找出账号对应的信息,并与前台传入的密码进行比对(shiro 将数据库中取出的密码跟 token 进行匹配)判断是否登录成功。这边还可以做其他的操作,比如账号的冻结判断等。具体实现代码如下:

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken authToken = (UsernamePasswordToken) token;
        // 获取用户输入的账号
        String userName = authToken.getUsername();
        Optional<SysUser> optional = userService.getUserByUserName(userName);

        if (!optional.isPresent()) {
            log.error("账号:{} 不存在!", userName);
            throw new UnknownAccountException(String.format("账号【%s】不存在!", userName));
        }

        SysUser user = optional.get();
        if (user.getStatus() == 0) {
            log.warn("账号:{} 已被冻结!", user.getUserName());
            // 账号冻结
            throw new LockedAccountException(String.format("账号【%s】已冻结!", userName));
        }
		// 封装 SimpleAuthenticationInfo 对象与 UsernamePasswordToken 对象进行密码对比
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getCredentialsSalt()),
                getName()
        );
        return authenticationInfo;
    }

doGetAuthorizationInfo:授权方法,shiro 并不是在认证之后就马上对用户授权,而是在认证通过之后,接下来要访问的资源或者目标方法需要权限的时候才会调用 doGetAuthorizationInfo() 方法进行授权。具体实现代码如下:

   @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("授权...");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        if (null == user) {
            log.error("授权失败,用户信息为空!!!");
            return null;
        }
        try {
            // 添加用户角色
            List<SysRole> roles = roleService.findRoleByUserId(user.getId());
            roles.forEach(role -> authorizationInfo.addRole(role.getRoleName()));

            // 添加用户权限
            List<SysAuthority> auths = authService.findAuthByUserId(user.getId());
            auths.forEach(auth -> authorizationInfo.addStringPermission(auth.getId()));
        } catch (Exception e) {
            log.error("出现异常啦!!", e);
        }
        return authorizationInfo;
    }

二、动态权限管理

1、权限动态加载
为了实现动态的权限配置,所以将链接的权限配置近数据库,然后在项目启动或者权限有变更(新增、删除、更新)的时候将数据库中的权限信息加载进 shiro。

package com.easytouch.easyworking.service.impl;

import com.easytouch.easyworking.entity.SysAuthority;
import com.easytouch.easyworking.mapper.SysAuthorityMapper;
import com.easytouch.easyworking.service.SysAuthorityService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;

/** * @author Hinbo * create date 2018/11/5 20:26 */
@Slf4j
@Service
public class SysAuthorityServiceImpl implements SysAuthorityService {

    @Autowired
    private SysAuthorityMapper authMapper;

    @Override
    public Map<String, String> loadFilterChainDefinitions() {
        List<SysAuthority> authorities = authMapper.findAuthorities();
        // 权限控制map.从数据库获取
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        if (authorities.size() > 0) {
            String uris;
            String[] uriArr;
            for (SysAuthority authority : authorities) {
                if (StringUtils.isBlank(authority.getPermission())) {
                    // 权限为空则不处理
                    continue;
                }
                uris = authority.getUri();
                uriArr = uris.split(";");
                for (String uri : uriArr) {
                    filterChainDefinitionMap.put(uri, authority.getPermission());
                }
            }
        }
        // 退出登录
        filterChainDefinitionMap.put("/logout", "logout");
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        return filterChainDefinitionMap;
    }

    @Override
    public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        synchronized (this) {
            AbstractShiroFilter shiroFilter;
            try {
                shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
            } catch (Exception e) {
                log.error("get ShiroFilter from shiroFilterFactoryBean error!");
                throw new RuntimeException("get ShiroFilter from shiroFilterFactoryBean error!");
            }

            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
            DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();

            // 清空旧的权限控制
            log.info("清空旧的权限控制...");
            manager.getFilterChains().clear();
            shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
            // 重新加载权限控制
            shiroFilterFactoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitions());

            // 重新构建权限控制的过滤链
            log.info("重新构建权限控制的过滤链...");
            Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue().trim().replace(" ", "");
                manager.createChain(url, chainDefinition);
            }
        }
    }
}

2、自定义角色过滤器
主要是实现对登录用户角色的校验。

public class CustomRolesAuthorizationFilter extends RolesAuthorizationFilter {

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;
        // 如果没有角色限制,直接放行
        if (rolesArray == null || rolesArray.length == 0) {
            return true;
        }
        // 判断是否有角色权限
        for (int idx = 0; idx < rolesArray.length; idx++) {
            if (subject.hasRole(rolesArray[idx])) {
                return true;
            }
        }
        return false;
    }
}

3、shiro 配置
shiro 提供了一系列的接口让我们实现对 shiro,包含 SessionManager、用户认证信息和用户信息缓存等。直接上代码

/** * Shiro 配置 * @author Hinbo * create date 2018/11/1 15:47 */
@Slf4j
@Configuration
public class ShiroConfig {

    @Autowired
    private RedisProperties redisProperties;
    @Autowired
    private SysAuthorityService authorityService;

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        log.info("Shiro 权限过滤...");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 权限过滤 Filter
        Map<String, Filter> filterMap = new LinkedHashMap<>(1);
        filterMap.put("roles", rolesAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        //配置 shiro 默认登录界面地址
        shiroFilterFactoryBean.setLoginUrl("/unLogin");

        // 配置无权限的访问地址
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(authorityService.loadFilterChainDefinitions());
        return shiroFilterFactoryBean;
    }

    @Bean
    public CustomRolesAuthorizationFilter rolesAuthorizationFilter() {
        return new CustomRolesAuthorizationFilter();
    }

    /** * 凭证匹配器(由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了) * @return HashedCredentialsMatcher */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 使用 MD5 散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列次数
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean
    public AuthShiroRealm authShiroRealm() {
        AuthShiroRealm authShiroRealm = new AuthShiroRealm();
        authShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return authShiroRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authShiroRealm());
        // 自定义 session 管理,使用 Redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现,使用 Redis
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    /** * 自定义 SessionManager * @return sessionManager */
    @Bean
    public SessionManager sessionManager() {
        EwSessionManager sessionManager = new EwSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /** * 配置shiro redisManager,使用的是shiro-redis开源插件 */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisProperties.getHost());
        redisManager.setPort(Integer.parseInt(redisProperties.getPort()));
        redisManager.setPassword(redisProperties.getPassword());
        redisManager.setDatabase(Integer.parseInt(redisProperties.getDatabase()));
        return redisManager;
    }

    /** * cacheManager 缓存 redis 实现 */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /** * RedisSessionDAO shiro sessionDao层的实现 通过redis */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setKeyPrefix("shiro:user:");
        return redisSessionDAO;
    }

    /** * 开启 Shiro aop 注解支持 * 用代理方式;所以需要开启代码支持 * @param securityManager securityManager * @return advisor */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

你可能感兴趣的:(Spring,Boot)