SpringSecurity integrates springBoot and redis to realize login kicking

background

Based on my article - "SpringSecurity integrates springBoot and redis token dynamic url permission verification". The function to be implemented is to prevent a user from logging in to two devices at the same time. There are two ideas:
(1) Subsequent logins will automatically kick out previous logins.
(2) If the user has already logged in, latecomers will not be allowed to log in.
It should be noted that the basis of the project is a session already maintained by redis.

Configure redisHttpSession

Set the spring session to be managed by redis.
2.1 Remove the http session configuration in yml, and choose only one of yml and annotations (configure at the same time, only the annotation configuration takes effect). As for why yml is not used, I will mention it later.
Insert image description here
2.2 Add the annotation @EnableRedisHttpSession to webSecurityConfig
Insert image description here

@EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 1700
        , flushMode = FlushMode.ON_SAVE)

After logging in, we found that the redis session namespace was already named by us.

Insert image description here

Get the sessionRepository managed by redis

If we want to restrict a user's login, we naturally need to obtain all his sessions in the system.

2. Check the documentation on the springsSession official website. The springsession official website provides documentation https://docs.spring.io/spring-session/docs/2.2.2.RELEASE/reference/html5/#api-findbyindexnamesessionrepository

SessionRepository implementations can also choose to implement FindByIndexNameSessionRepository

FindByIndexNameSessionRepository provides a method to find all sessions with a given index name and index value

FindByIndexNameSessionRepository implementation provides a convenience method to find all sessions for a specific user

/**
     * redis获取sessionRepository
     * RedisIndexedSessionRepository实现 FindByIndexNameSessionRepository接口
     */
    @Autowired
    //不加@Lazy这个会报什么循环引用...
    // Circular reference involving containing bean '.RedisHttpSessionConfiguration' 
    @Lazy   
    private FindByIndexNameSessionRepository<? extends Session> sessionRepository;

One thing to note here is that when I configure redis session through yml, there will be a red line under sessionRepository.
Insert image description here
Although it does not affect the operation, it is obsessive-compulsive, so I use the @EnableWebSecurity annotation instead (as for why? I don’t want to know...).

Inject sessionRepository into SpringSessionBackedSessionRegistry

It is the session concurrency session registry implementation provided by spring session for Spring Security. It probably allows springSecurity to help us restrict logins. A sessionRepository alone is not enough, and we have to add some tools ourselves.
webSecurityConfig added:

/**
     * 是spring session为Spring Security提供的,
     * 用于在集群环境下控制会话并发的会话注册表实现
     * @return
     */
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(){
    
    
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

Note:
https://blog.csdn.net/qq_34136709/article/details/106012825 This article says that you need to add an HttpSessionEventPublisher to monitor session destruction. This is probably because I use redis session. I don’t need this. After that, an error will still be reported. What's wrong? I forgot.

Add a new processing class after session expiration

First create a CustomSessionInformationExpiredStrategy.java to handle how to notify the front-end processing class after the session expires. The content is as follows:

public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    
    

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
    
    
        if (log.isDebugEnabled()) {
    
    
           log.debug("{} {}", event.getSessionInformation(), MessageConstant.SESSION_EVICT);
        }
        HttpServletResponse response = event.getResponse();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.SESSION_EVICT, MessageConstant.SESSION_EVICT));
        response.getWriter().write(responseJson);
    }
}

Note: Generally, you rewrite the information returned to the front end yourself, and do not directly use the error information thrown by the framework.

Configure it on the configure(HttpSecurity http) method

.csrf().disable()
//登录互踢
.sessionManagement()
//在这里设置session的认证策略无效
//.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry()))
.maximumSessions(1)
.sessionRegistry(sessionRegistry())
.maxSessionsPreventsLogin(false) //false表示不阻止登录,就是新的覆盖旧的
//session失效后要做什么(提示前端什么内容)
.expiredSessionStrategy(new CustomSessionInformationExpiredStrategy()); 

Note: https://blog.csdn.net/qq_34136709/article/details/106012825 This article talks about the principle of session authentication. I saw that it implemented a session authentication strategy, but when I debugged the corresponding code, I found that
Insert image description here
This session authentication strategy is NullAuthenticatedSessionStrategy, not the ConcurrentSessionControlAuthenticationStrategy it says. That is to say, where do I need to configure this session authentication policy. The first thing I thought of was that the configuration in configure(HttpSecurity http)
Insert image description here
turned out to be invalid. After seeing other people's code, I thought that this strategy should be added when logging in, and our login generally needs to be rewritten by ourselves, so naturally the above writing method will be invalid. So I found custom login filters.
Insert image description here
Insert image description here
Then I found that this.setSessionAuthenticationStrategy(sessionStrategy); does exist.

public LoginFilter(UserVerifyAuthenticationProvider authenticationManager,
                       CustomAuthenticationSuccessHandler successHandler,
                       CustomAuthenticationFailureHandler failureHandler,
                       SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry) {
    
    
        //设置认证管理器(对登录请求进行认证和授权)
        this.authenticationManager = authenticationManager;
        //设置认证成功后的处理类
        this.setAuthenticationSuccessHandler(successHandler);
        //设置认证失败后的处理类
        this.setAuthenticationFailureHandler(failureHandler);
        //配置session认证策略(将springSecurity包装redis Session作为参数传入)
        ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new
                ConcurrentSessionControlAuthenticationStrategy(springSessionBackedSessionRegistry);
        //最多允许一个session
        sessionStrategy.setMaximumSessions(1);
        this.setSessionAuthenticationStrategy(sessionStrategy);
        //可以自定义登录请求的url
        super.setFilterProcessesUrl("/myLogin");
    }

After startup, we found that the session authentication policy has been changed to the policy we set.

The complete webSecurityConfig is as follows:

@Configuration
@EnableWebSecurity
//RedisFlushMode有两个参数:ON_SAVE(表示在response commit前刷新缓存),IMMEDIATE(表示只要有更新,就刷新缓存)
//yml和注解两者只选其一(同时配置,只有注解配置生效)
@EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 5000
        , flushMode = FlushMode.ON_SAVE)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

    @Autowired
    private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

    @Autowired
    private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

    @Autowired
    private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
    @Autowired
    private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验


    /**
     * redis获取sessionRepository
     * RedisIndexedSessionRepository实现 FindByIndexNameSessionRepository接口
     */
    @Autowired
    //不加@Lazy这个会报什么循环引用...
    // Circular reference involving containing bean '.RedisHttpSessionConfiguration'
    @Lazy
    private FindByIndexNameSessionRepository<? extends Session> sessionRepository;


    /**
     * 是spring session为Spring Security提供的,
     * 用于在集群环境下控制会话并发的会话注册表实现
     * @return
     */
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(){
    
    
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

    /**
     * 密码加密
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(PasswordEncoder.class)
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置 HttpSessionIdResolver Bean
     * 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
     * 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
     */
    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
    
    
        return HeaderHttpSessionIdResolver.xAuthToken();
    }






    /**
     * Swagger等静态资源不进行拦截
     */
    @Override
    public void configure(WebSecurity web) {
    
    
        web.ignoring().antMatchers(
                "/*.html",
                "/favicon.ico",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js",
                "/error",
                "/webjars/**",
                "/resources/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.authorizeRequests()
                //配置一些不需要登录就可以访问的接口,这里配置失效了,放到了securityMetadataSource里面
                //.antMatchers("/demo/**", "/about/**").permitAll()
                //任何尚未匹配的URL只需要用户进行身份验证
                .anyRequest().authenticated()
                //登录后的接口权限校验
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setAccessDecisionManager(accessDecisionManager);
                        object.setSecurityMetadataSource(securityMetadataSource);
                        return object;
                    }
                })
                .and()
                //配置登出处理
                .logout().logoutUrl("/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .clearAuthentication(true)
                .and()
                //用来解决匿名用户访问无权限资源时的异常
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                //用来解决登陆认证过的用户访问无权限资源时的异常
                .accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                //配置登录过滤器
                .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler, sessionRegistry()))

                .csrf().disable()
                //登录互踢
                .sessionManagement()
                //在这里设置session的认证策略无效
                //.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry()))
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .maxSessionsPreventsLogin(false) //false表示不阻止登录,就是新的覆盖旧的
                //session失效后要做什么(提示前端什么内容)
                .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy());
        //配置头部
        http.headers()
                .contentTypeOptions()
                .and()
                .xssProtection()
                .and()
                //禁用缓存
                .cacheControl()
                .and()
                .httpStrictTransportSecurity()
                .and()
                //禁用页面镶嵌frame劫持安全协议  // 防止iframe 造成跨域
                .frameOptions().disable();
    }




}

other

@Lazy
private FindByIndexNameSessionRepository<? extends Session> sessionRepository;

As for the issue of circular references if @lazy is not added, I really don’t want to pay attention to it. After watching it for a long time, I didn’t know who had a circular reference with whom. . . . .

Guess you like

Origin blog.csdn.net/mofsfely2/article/details/116569316