关于SpringBoot整合Shiro并入redis缓存
最近做一个小项目加入shiro权限框架, Shiro是Apache下的一个开源项目,提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。
记录一下开发过程。。。
1.开发环境:
JDK:1.8
SpringBoot:2.4.4
2. 加入依赖:
<!--springboot中的redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 使用jedis需要对新版本的redis进行屏蔽<exclusions>-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version>
</dependency>
<!--shiro权限-->
<dependency>
<!--session持久化插件-->
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<!--spring shiro依赖-->
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
3. ShiroConfiguration-Shiro 配置
package com.carshow.data.shior;
import java.util.LinkedHashMap;
import java.util.Map;
import com.carshow.data.common.Common;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
@Configuration
public class ShiroConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
logger.info("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
filterChainDefinitionMap.put("/auth/kickout", "anon");
filterChainDefinitionMap.put(Common.PATH_OUT_URL+"**", "anon");
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc,kickout");
shiroFilterFactoryBean.setLoginUrl(Common.PATH_OUT_URL+"user/un_auth");
// 未授权界面, 对应LoginController中 unauthorized 请求
shiroFilterFactoryBean.setUnauthorizedUrl(Common.PATH_OUT_URL+"user/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:");
return redisCacheManager;
}
public RedisManager redisManager() {
logger.info("===============创建RedisManager,连接Redis..URL= " + redisHost + ":" + redisPort);
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
redisManager.setTimeout(1800);
redisManager.setPassword(redisPassword);
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:");
return redisSessionDAO;
}
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
@Bean
public DefaultWebSessionManager sessionManager() {
MySessionManager sessionManager = new MySessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
kickoutSessionControlFilter.setCacheManager(cacheManager());
kickoutSessionControlFilter.setSessionManager(sessionManager());
kickoutSessionControlFilter.setKickoutAfter(false);
kickoutSessionControlFilter.setMaxSession(1);
kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
return kickoutSessionControlFilter;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
4.身份校验核心类-MyShiroRealm
package com.carshow.data.shior;
import com.carshow.data.mapper.RoleMapper;
import com.carshow.data.mapper.UserMapper;
import com.carshow.data.model.TPermission;
import com.carshow.data.model.TRole;
import com.carshow.data.model.TUser;
import com.carshow.data.service.IRoleService;
import com.carshow.data.service.IUserService;
import com.carshow.data.service.impl.UserServiceImpl;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
private static String SALT = "mySlalt";
@Resource
private IUserService userService;
@Resource
private IRoleService roleService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("---------------- 执行 Shiro 凭证认证 ----------------------");
String username = (String)token.getPrincipal();
TUser user = userService.selectUserByName(username);
if (Objects.isNull(user)) {
return null;
}
logger.info("----->>userInfo=" + user.toString());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username,
user.getPassword(),
ByteSource.Util.bytes(user.getUsername() + SALT),
getName()
);
return authenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("开始执行授权操作-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
TUser user=userService.selectUserByName(principals.getPrimaryPrincipal().toString());
List<TRole> roles = roleService.getUserRoles(user.getId());
for(TRole role:roles){
authorizationInfo.addRole(role.getName());
for(TPermission p:role.getPermissions()){
authorizationInfo.addStringPermission(p.getCode());
}
}
return authorizationInfo;
}
}
5. 自定义session管理-MySessionManager
package com.carshow.data.shior;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
}
6. 限制并发人数登陆–KickoutSessionControlFilter
package com.carshow.data.shior;
import com.alibaba.fastjson.JSON;
import com.carshow.data.model.TUser;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
public class KickoutSessionControlFilter extends AccessControlFilter {
private String kickoutUrl;
private boolean kickoutAfter = false;
private int maxSession = 1;
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro_redis_cache");
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
Deque<Serializable> deque = cache.get(username);
if(deque==null){
deque = new LinkedList<Serializable>();
}
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
cache.put(username, deque);
}
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) {
kickoutSessionId = deque.removeFirst();
cache.put(username, deque);
} else {
kickoutSessionId = deque.removeLast();
cache.put(username, deque);
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
}
}
if (session.getAttribute("kickout") != null) {
try {
subject.logout();
} catch (Exception e) {
}
saveRequest(request);
Map<String, String> resultMap = new HashMap<String, String>();
if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
resultMap.put("user_status", "300");
resultMap.put("message", "您已经在其他地方登录,请重新登录!");
out(response, resultMap);
}else{
WebUtils.issueRedirect(request, response, kickoutUrl);
}
return false;
}
return true;
}
private void out(ServletResponse hresponse, Map<String, String> resultMap)
throws IOException {
try {
hresponse.setCharacterEncoding("UTF-8");
PrintWriter out = hresponse.getWriter();
out.println(JSON.toJSONString(resultMap));
out.flush();
out.close();
} catch (Exception e) {
System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
}
}
}
7. 用户登录-UserServiceImpl
@Override
public ResultBody login(TUser user, HttpServletRequest request, HttpServletResponse response) {
String msg ;
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
token.setRememberMe(true);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
if (subject.isAuthenticated()) {
System.out.println("========="+subject.getSession().getId());
return ResultBody.success(subject.getSession().getId());
} else {
return ResultBody.error("登录失败");
}
} catch (IncorrectCredentialsException e) {
msg = "登录密码错误. Password for account " + token.getPrincipal() + " was incorrect.";
} catch (ExcessiveAttemptsException e) {
msg = "登录失败次数过多";
} catch (LockedAccountException e) {
msg = "帐号已被锁定. The account for username " + token.getPrincipal() + " was locked.";
} catch (DisabledAccountException e) {
msg = "帐号已被禁用. The account for username " + token.getPrincipal() + " was disabled.";
} catch (ExpiredCredentialsException e) {
msg = "帐号已过期. the account for username " + token.getPrincipal() + " was expired.";
} catch (UnknownAccountException e) {
msg = "帐号不存在. There is no user with username of " + token.getPrincipal();
} catch (UnauthorizedException e) {
msg = "您没有得到相应的授权!" + e.getMessage();
}
log.info(msg);
return ResultBody.error(msg);
}
未授权及未登录
@RequestMapping("/un_auth")
public ResultBody unAuth() {
return ResultBody.error( "用户未登录!");
}
@RequestMapping("/unauthorized")
public ResultBody unauthorized() {
return ResultBody.error( "403","用户无权限!");
}
登出
@Override
public ResultBody logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return ResultBody.success("登出成功");
}
8.关于redis配置文件-properties
# Redis 数据库索引(默认为 0)
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis 服务器连接密码(默认为空)
spring.redis.password=xxx
# springboot 2.0 redis默认客户端已换成lettuce
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
spring.redis.timeout=5000
# 存储在redis中的键名
spring.session.redis.namespace=session:redis
测试及使用




目前到此完结,符合预期
感谢
参考文档:
springboot整合shiro应用
Springboot2.0 集成shiro权限管理