开发者

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定时清理会话任务,如果不配置,默认是每分钟执行一次

SpringSecurity导致Redis压力大问题的解决方案

方案

分析出结果后,解决方案也呼之欲出了

定时任务处理

仅在少数特定的应用中保留 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的拦截器加载顺序

序号过滤器名称说明
1SecurityContextPersistenceFilter恢复安全上下文,从会话或请求中加载安全信息,为后续过滤器的执行奠定基础。
2HeaderWriterFilter添加安全相关的 HTTP 响应头,如内容安全策略(CSP)、HTTP 严格传输安全(HSTS)等,增强应用的安全性。
3CorsFilter处理跨域资源共享(CORS)请求,验证跨域请求的合法性。
4CsrfFilter验证 CSRF 令牌,防止跨站请求伪造攻击,保护应用免受恶意请求的侵害。
5LogoutFilter处理注销请求,使用户能够安全地退出应用,清除相关的安全上下文。
6UsernamePasswordAuthenticationFilter处理基于用户名和密码的登录请求,验证用户的身份,并在验证成功后创建认证对象。
7DefaultLoginPageGeneratingFilter如果未配置自定义登录页,生成默认的登录页面,简化了登录流程的实现。
8BasicAuthenticationFilter处理 HTTP Basic 认证,验证请求中的认证信息,并在验证成功后创建认证对象。
9BearerTokenAuthenticationFilter提取 JWT 等令牌进行认证,适用于基于令牌的认证机制。
10RequestCacheAwareFilter恢复缓存的请求,以便在用户登录后能够正确地重定向到原始请求的页面。
11SecurityContextHolderFilter使安全上下文在请求中可用,为后续的安全操作提供上下文支持。
12AnonymousAuthenticationFilter如果未找到认证信息,则分配一个匿名用户认证对象,使匿名用户也能够访问应用中的某些资源。
13SessionManagementFilter处理会话固定防护和并发控制,管理会话相关的安全策略,如防止会话劫持等。
14ExceptionTranslationFilter捕获安全异常并将其转换为相应的 HTTP 响应,如未认证或未授权的错误响应,使应用能够以合适的方式处理安全问题。
15FilterSecurityInterceptor强制执行授权规则,检查用户是否有权限访问请求的资源,是 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)其它相关文章!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新开发

开发排行榜