SpringSecurity导致Redis压力大问题的解决方案
目录
- 分析
- 总体分析
- 简单介绍下springsecuriy用到的Rediskey
- 抽丝剥茧,命中难点
- 方案
- 定时任务处理
- cookie过多治理
- 总体方案
- 生成自定义session处理拦截器
- 配置拦截器
分析
总体分析
经过深入分析,我发现 Redis 的 hgetallwww.devze.com 操作极为频繁,而这些操作主要是由 Spring Security 框架执行,主要是hgetall 获取spring:session:expirations:{timestamp}中的所有会话、一是hgetall获取的数据量过大,二是执行hgetall过于频繁。
简单介绍下springsecuriy用到的rediskey
Redis Key | 说明 |
spring:session:sessions:{sessionId} | Spring Session 的默认存储 Key,用于存储每个会话的信息,其中 {sessionId} 是具体的会话 ID。 |
spring:session:sessions:expires:{sessionId} | 存储会话的过期时间戳,与具体的会话 ID 相关联。 |
spring:session:expirations:{timestamp} | 存储每分钟需要过期的会话 ID 集合,{timestamp} 是每分钟的时间戳。 |
spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME: | 索引 Key,用于存储用户名与会话 ID 的映射关系,方便根据用户名快速查找对应的会话 ID 集合。 |
抽丝剥茧,命中难点
进一步调查发现,我们的系统目前存在两套鉴权体系,一套基于 token,另一套基于 cookie。虽然 token 鉴权体系本身并未直接使用 Redis,但由于集成了 Spring Security 框架,token 类型同样会生成 cookie,并由此对 Redis 进行读写操作。
在实际使用中,采用 cookie 鉴权方式的主要是管理系统,其使用人员相对较少;而 token 鉴权方式的使用量却极为庞大,是导致 Redis CPU 占用率居高不下的关键因素。此外,我们所有应用都默认开启了 “Spring 定时清理过期会话” 的任务,这一任务也导致了 QPS(每秒查询率)过高,进一步加剧了 Redis 的负载。
springsecurity定时清理会话任务,如果不配置,默认是每分钟执行一次
方案
分析出结果后,解决方案也呼之欲出了
定时任务处理
仅在少数特定的应用中保留 session 过期处理机制。
如果是springboot2及以上,则可以通过配置关掉。而我们用的springboot1,配置无法关掉,只能另辟蹊径,把非登录相关应用的清理时间都改为一天一次或一月一次。修改方式如下
yml中配置
spring: session: cleanup: cron: # spring session 定期清理redis关闭,这个定时任务用户中http://www.devze.com心开启即可 # expression: '-' springboot2.0才支持 expression: '0 02 02 ? * *'
cookie过多治理
总体方案
对于采用 token 鉴权方式的系统,不再生成 cookie,避免对 Redis 进行读写操作。
因为springsecurity默认使用spring-session中的 SessionRepositoryFilter来进行session的操作,所以我们需要生成一个自定义session处理拦截器,来覆盖springsecurity自身的处理,如果header中存在token,就不再执行SessionRepositoryFilter拦截器。就不会对redis进行任何操作。
我们需要添加拦截器,需要他在SessionManagementFilter拦截器执行前执行,然后重新打sso包,让各应用更新使用(SessionRepositoryFilter
并非 Spring Security 默认过滤器链的一部分,它通常是通过 spring-session
项目引入的,以便在分布式应用中共享会话信息。在实际使用中,SessionRepositoryFilter
通常在 SessionManagementFilter
之前执行,以便能够正确地管理和存储会话信息)
这里简单介绍下springsecurity的拦截器加载顺序
序号 | 过滤器名称 | 说明 |
1 | SecurityContextPersistenceFilter | 恢复安全上下文,从会话或请求中加载安全信息,为后续过滤器的执行奠定基础。 |
2 | HeaderWriterFilter | 添加安全相关的 HTTP 响应头,如内容安全策略(CSP)、HTTP 严格传输安全(HSTS)等,增强应用的安全性。 |
3 | CorsFilter | 处理跨域资源共享(CORS)请求,验证跨域请求的合法性。 |
4 | CsrfFilter | 验证 CSRF 令牌,防止跨站请求伪造攻击,保护应用免受恶意请求的侵害。 |
5 | LogoutFilter | 处理注销请求,使用户能够安全地退出应用,清除相关的安全上下文。 |
6 | UsernamePasswordAuthenticationFilter | 处理基于用户名和密码的登录请求,验证用户的身份,并在验证成功后创建认证对象。 |
7 | DefaultLoginPageGeneratingFilter | 如果未配置自定义登录页,生成默认的登录页面,简化了登录流程的实现。 |
8 | BasicAuthenticationFilter | 处理 HTTP Basic 认证,验证请求中的认证信息,并在验证成功后创建认证对象。 |
9 | BearerTokenAuthenticationFilter | 提取 JWT 等令牌进行认证,适用于基于令牌的认证机制。 |
10 | RequestCacheAwareFilter | 恢复缓存的请求,以便在用户登录后能够正确地重定向到原始请求的页面。 |
11 | SecurityContextHolderFilter | 使安全上下文在请求中可用,为后续的安全操作提供上下文支持。 |
12 | AnonymousAuthenticationFilter | 如果未找到认证信息,则分配一个匿名用户认证对象,使匿名用户也能够访问应用中的某些资源。 |
13 | SessionManagementFilter | 处理会话固定防护和并发控制,管理会话相关的安全策略,如防止会话劫持等。 |
14 | ExceptionTranslationFilter | 捕获安全异常并将其转换为相应的 HTTP 响应,如未认证或未授权的错误响应,使应用能够以合适的方式处理安全问题。 |
15 | FilterSecurityInterceptor | 强制执行授权规则,检查用户是否有权限访问请求的资源,是 Spring Security 授权过程的核心。 |
以下是为实现该解决方案所编写的代码:
生成自定义session处理拦截器
创建一个MySessionRepositoryFilter继承SessionRepositoryFilter
package com.onlylowg.config.filter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.session.SessionRepository; import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.stereotype.Component; import Javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * token filter */ @Order(SessionRepositoryFilter.DEFAULT_ORDER) @Component public class MySessionRepositoryFilter extends SessionRepositoryFilter { private String tokenHeader = "Authorization"; private String tokenHead = "bearer"; private String AccessToken = "access_token"; /** * Creates a new instance. * * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null. */ public MySessionRepositoryFilter(SessionRepository sessionRepository) { super(sessionRepository); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!StringUtils.isNotBlank(request.getHeader(tokenHeader))) { super.doFilterInternal(request, response, filterChain); return; } filterChain.doFilter(request, response); } }
配置拦截器
配置MySessionRepositoryFilter在SessionManagementFilter拦截器执行前执行
package com.onlylowg.config; import com.onlylowg.config.filter.MySessionRepositoryFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.session.*; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.security.SpringSessionBackedSessionRegistry; import javax.annotation.Resource; /** * spring security配置类 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { // jwt: // header: Authorization // tokenHead: bearer // accessToken: access_token private String tokenHeader = "Authorization"; private MySessionRepositoryFilter mySessionRepositoryFilter; private FindByIndexNameSessionRepository sessionRepository; * @date 2017-08-29 * 这里这样注入,避免被扫包时加入到fitlers中,然后手动加入拦截器,设置在UsernamePasswordAuthenticationFilter之前执行 * 注意JwtAuthenticationTokenFilter 不能加注解,加的话,jwt会工作两次 */ @Bean("jwtAuthenticationTokenFilter") public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() throws Exception { return new J编程客栈wtAuthjavascriptenticationTokenFilter(); } /** * 配置资源服务器请求相关 * * @param http 请求 * @throws Exception 异常 * @author WangHQ * @date 2017-07-13 */ @Override protected void configure(HttpSecurity http) throwswww.devze.com Exception { init(); http .csrf().disable() //.requestMatcher(new OAuthRequestedMatcher()) .authorizeRequests() .antMatchers("/**").permitAll()//允许所有访问,权限控制交由filter .anyRequest().authenticated() ; http.addFilterBefore(mySessionRepositoryFilter, SessionManagementFilter.class); } public void init() { mySessionRepositoryFilter = new MySessionRepositoryFilter(sessionRepository); } }
以上就是SpringSecurity导致Redis压力大问题的解决方案的详细内容,更多关于SpringSecurity导致Redis压力大的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论