SpringSecurity导致redis压力大问题解决

背景

在我们现有的系统中,用于登录的 Redis 服务器 CPU 占用率长期处于 90% 的高位,这一状况带来了极大的风险隐患。因此,领导委派给我一项艰巨的任务,旨在解决这一紧迫问题。

分析

总体分析

经过深入分析,我发现 Redis 的 hgetall 操作极为频繁,而这些操作主要是由 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中配置

yaml 复制代码
spring:
  session:
    cleanup:
      cron:
        # spring session 定期清理redis关闭,这个定时任务用户中心开启即可
        # 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

java 复制代码
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拦截器执行前执行

kotlin 复制代码
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 JwtAuthenticationTokenFilter();
    }

    /**
     * 配置资源服务器请求相关
     *
     * @param http 请求
     * @throws Exception 异常
     * @author WangHQ
     * @date 2017-07-13
     */
    @Override
    protected void configure(HttpSecurity http) throws 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);
    }
}

相关推荐
free-9d4 小时前
NodeJs后端常用三方库汇总
后端·node.js
写不出来就跑路6 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
天上掉下来个程小白6 小时前
MybatisPlus-06.核心功能-自定义SQL
java·spring boot·后端·sql·微服务·mybatisplus
知了一笑6 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
寻月隐君7 小时前
想用 Rust 开发游戏?这份超详细的入门教程请收好!
后端·rust·github
晴空月明7 小时前
分布式系统高可用性设计 - 缓存策略与数据同步机制
后端
Real_man8 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小马爱打代码8 小时前
Spring Boot:将应用部署到Kubernetes的完整指南
spring boot·后端·kubernetes
卜锦元8 小时前
Go中使用wire进行统一依赖注入管理
开发语言·后端·golang
SoniaChen3310 小时前
Rust基础-part3-函数
开发语言·后端·rust