一,实际使用介绍
在实际开发中使用shiro,那就要考虑集群部署和多端登录问题。之前出了一篇简单使用,为了让我们初步对shiro认识。真实使用那么就要shiro原码和执行流程要有一个基础的了解,不然就不知从哪里下手。
开发中的集群部署和多端登录问题,主要考虑的就是Session。
Session 运行流程主要涉及到用户认证、授权、会话创建、管理及销毁等多个方面。处理好Session那么集群部署和多端登录问题也就得以解决。
二,shiro架构简单介绍
subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
authenticator:认证器,主体进行认证最终通过authenticator进行的。
authorizer:授权器,主体进行授权最终通过authorizer进行的。
sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
依据架构介绍,那么与session有关的就是sessionManager,SessionDao,cacheManager这三个类,那么解决session问题这三个类是关键。
三,问题处理思路
1,集群部署
集群部署后需要考虑如何在多个应用服务器之间共享和同步会话状态、权限和认证信息。简单的解说的session共享。
这里的解决方案是
分布式缓存:
使用Redis、Memcached等分布式缓存来存储Shiro的会话、权限和认证信息。这样,所有应用服务器都可以访问这些缓存中的数据,从而实现了信息的共享和同步。
需要实现自定义的Shiro组件(如CacheManager、SessionDAO等),以便与分布式缓存进行集成。
代码实现:
package com.zwxict.user.shiro;
import com.zwxict.common.utils.StringUtils;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* Shiro配置类
* @author 汤义
* @create 2024-03-17:53
*/
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Value("${shiro.session.expire}")
private int expire;
// 配置Realm
@Bean
public ShiroRealm realm() {
return new ShiroRealm();
}
// 配置SecurityManager
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
// 配置ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 配置拦截规则
Map<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/userLogin/login", "anon");
filterChainDefinitionMap.put("/userRegister/register", "anon");
filterChainDefinitionMap.put("/userLogin/shiro_login", "anon");
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filterFactoryBean;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setExpire(expire);//缓存过期时间:秒
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
redisManager.setTimeout(30000);//连接redis超时
if(StringUtils.isNotEmpty(redisPassword))
redisManager.setPassword(redisPassword);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setExpire(expire);//session会话过期时间,默认就是1800秒
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
application.yml配置
spring:
#Redis
redis:
host: XXXXXX #redis服务IP
port: 6379 #端口
password: XXXX #密码
timeout: 10000
jedis:
pool:
max-wait: 61000ms
max-active: 1000
#session过期时间:秒
shiro:
session:
expire: 1800
redis-record:
user:
password: 5
这就是session共享代码实现。
2,多端登录
多端登录,那么如何保证Session的认证了。
在使用Shiro时,有没发现Cookie,在登录之后会返回一个Cookie,其它接口访问是带上没有过期的Cookie就能访问成功。
这个Cookie就与Session有关联,是在登录成功后Shiro会生成一个Session,Session中会产生一个sessionId。sessionId就是Cookie中JSESSIONID=的后面一节字符。
那么维护好Cookie中JSESSIONID是不是就可以实现多端登录认证了,那么只要传真确的sessionId,就能保证认证成功了。
在源码中可以对sessionId进行干预的是DefaultWebSessionManager。继承DefaultWebSessionManager类重新一个类来做这个操作。
package com.zwxict.user.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.servlet.ShiroHttpSession;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
/**
* @author 汤义
* @create 2024-03-14:21
*/
@Slf4j
public class CustomerWebSessionManager extends DefaultWebSessionManager {
private static final String AUTH_TOKEN = "token";
private static final String DEVICE = "token1";
private static final String MOBILE = "token2";
public CustomerWebSessionManager() {
super();
}
/**
* 重写父类获取sessionID的方法,若请求为APP或者H5则从请求头中取出token
*
* @param request 请求参数
* @param response 响应参数
* @return id
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID. Returning null.");
return null;
}
HttpServletRequest httpRequest = WebUtils.toHttp(request);
if (StringUtils.hasText(httpRequest.getHeader(AUTH_TOKEN))) {
//从header中获取token
String token = httpRequest.getHeader(AUTH_TOKEN);
// 每次读取之后都把当前的token放入response中
HttpServletResponse httpResponse = WebUtils.toHttp(response);
if (StringUtils.hasText(token)) {
httpResponse.setHeader(AUTH_TOKEN, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
//sessionIdUrlRewritingEnabled的配置为false,不会在url的后面带上sessionID
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return token;
}
return getReferencedSessionId(request, response);
}
/**
* shiro默认从cookie中获取sessionId
*
* @param request 请求参数
* @param response 响应参数
* @return
*/
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
} else {
//not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
//try the URI path segment parameters first:
id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
if (id == null) {
//not a URI path segment parameter, try the query parameters:
String name = getSessionIdName();
id = request.getParameter(name);
if (id == null) {
//try lowercase:
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
//automatically mark it valid here. If it is invalid, the
//onUnknownSession method below will be invoked and we'll remove the attribute at that time.
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
// always set rewrite flag - SHIRO-361
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return id;
}
/**
* copy from DefaultWebSessionManager
*
* @param request 请求参数
* @param response 响应参数
* @return
*/
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
if (!isSessionIdCookieEnabled()) {
log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
return null;
}
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null.");
return null;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
}
private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
if (!(servletRequest instanceof HttpServletRequest)) {
return null;
}
HttpServletRequest request = (HttpServletRequest) servletRequest;
String uri = request.getRequestURI();
if (uri == null) {
return null;
}
int queryStartIndex = uri.indexOf('?');
if (queryStartIndex >= 0) {
uri = uri.substring(0, queryStartIndex);
}
int index = uri.indexOf(';');
if (index < 0) {
//no path segment params - return:
return null;
}
//there are path segment params, let's get the last one that may exist:
final String TOKEN = paramName + "=";
uri = uri.substring(index + 1);
//we only care about the last JSESSIONID param:
index = uri.lastIndexOf(TOKEN);
if (index < 0) {
//no segment param:
return null;
}
uri = uri.substring(index + TOKEN.length());
index = uri.indexOf(';');
if (index >= 0) {
uri = uri.substring(0, index);
}
return uri;
}
private String getSessionIdName() {
String name = this.getSessionIdCookie() != null ? this.getSessionIdCookie().getName() : null;
if (name == null) {
name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
}
return name;
}
/**
* 存储会话id到response header中
*
* @param currentId 会话ID
* @param request HttpServletRequest
* @param response HttpServletResponse
*/
private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
}
String idString = currentId.toString();
//增加判断,如果请求头中包含DEVICE=MOBILE,则将sessionId放在header中返回
if (StringUtils.hasText(request.getHeader(DEVICE)) && MOBILE.equals(request.getHeader(DEVICE))) {
response.setHeader(AUTH_TOKEN, idString);
} else {
Cookie template = getSessionIdCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(idString);
cookie.saveTo(request, response);
}
log.trace("Set session ID cookie for session with id {}", idString);
}
/**
* 设置deleteMe到response header中
*
* @param request request
* @param response HttpServletResponse
*/
private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.hasText(request.getHeader(AUTH_TOKEN))) {
response.setHeader(AUTH_TOKEN, Cookie.DELETED_COOKIE_VALUE);
} else {
getSessionIdCookie().removeFrom(request, response);
}
}
/**
* 会话创建
* Stores the Session's ID, usually as a Cookie, to associate with future requests.
*
* @param session the session that was just {@link #createSession created}.
*/
@Override
protected void onStart(Session session, SessionContext context) {
super.onStart(session, context);
if (!WebUtils.isHttp(context)) {
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +
"pair. No session ID cookie will be set.");
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
if (isSessionIdCookieEnabled()) {
Serializable sessionId = session.getId();
storeSessionId(sessionId, request, response);
} else {
log.debug("Session ID cookie is disabled. No cookie has been set for new session with id {}", session.getId());
}
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
/**
* 会话失效
*
* @param s Session
* @param ese ExpiredSessionException
* @param key SessionKey
*/
@Override
protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
super.onExpiration(s, ese, key);
onInvalidation(key);
}
@Override
protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
super.onInvalidation(session, ise, key);
onInvalidation(key);
}
private void onInvalidation(SessionKey key) {
ServletRequest request = WebUtils.getRequest(key);
if (request != null) {
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
}
if (WebUtils.isHttp(key)) {
log.debug("Referenced session was invalid. Removing session ID cookie.");
removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
} else {
log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
"pair. Session ID cookie will not be removed due to invalidated session.");
}
}
/**
* 会话销毁
*
* @param session Session
* @param key SessionKey
*/
@Override
protected void onStop(Session session, SessionKey key) {
super.onStop(session, key);
if (WebUtils.isHttp(key)) {
HttpServletRequest request = WebUtils.getHttpRequest(key);
HttpServletResponse response = WebUtils.getHttpResponse(key);
log.debug("Session has been stopped (subject logout or explicit stop). Removing session ID cookie.");
removeSessionIdCookie(request, response);
} else {
log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
"pair. Session ID cookie will not be removed due to stopped session.");
}
}
}
登录时返回sessionId就可以了。