最近学了 Apache Shiro,刚好有个新项目需要完成,所以整理一下如何通过整合 SpringBoot + Shiro + Redis 实现登录认证和动态的权限管理的知识点。
实现过程中主要有两个问题待解决:
本文主要是对下面这两个问题进行解决。
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;
}
}