关于springsecurity:Spring-Security6-全新写法大变样

3次阅读

共计 18980 个字符,预计需要花费 48 分钟才能阅读完成。

@[toc]
Spring Security 在最近几个版本中配置的写法都有一些变动,很多常见的办法都废除了,并且将在将来的 Spring Security7 中移除,因而松哥在去年旧文的根底之上,又补充了一些新的内容,从新发一下,供各位应用 Spring Security 的小伙伴们参考。

接下来,我把从 Spring Security5.7 开始(对应 Spring Boot2.7 开始),各种已知的变动都来和小伙伴们梳理一下。

1. WebSecurityConfigurerAdapter

首先第一点,就是各位小伙伴最容易发现的 WebSecurityConfigurerAdapter 过期了,在目前最新的 Spring Security6.1 中,这个类曾经齐全被移除了,想凑合着用都不行了。

精确来说,Spring Security 是在 5.7.0-M2 这个版本中将 WebSecurityConfigurerAdapter 过期的,过期的起因是因为官网想要激励各位开发者应用基于组件的平安配置。

那么什么是基于组件的平安配置呢?咱们来举几个例子:

以前咱们配置 SecurityFilterChain 的形式是上面这样:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}

那么当前就要改为上面这样了:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();}

}

如果懂之前的写法的话,上面这个代码其实是很好了解的,我就不做过多解释了,不过还不懂 Spring Security 根本用法的小伙伴,能够在公众号后盾回复 ss,有松哥写的教程。

以前咱们配置 WebSecurity 是这样:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) {web.ignoring().antMatchers("/ignore1", "/ignore2");
    }

}

当前就得改成上面这样了:

@Configuration
public class SecurityConfiguration {

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
    }

}

另外还有一个就是对于 AuthenticationManager 的获取,以前能够通过重写父类的办法来获取这个 Bean,相似上面这样:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
    }
}

当前就只能本人创立这个 Bean 了,相似上面这样:

@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

    @Bean
    AuthenticationManager authenticationManager() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager pm = new ProviderManager(daoAuthenticationProvider);
        return pm;
    }
}

当然,也能够从 HttpSecurity 中提取进去 AuthenticationManager,如下:

@Configuration
public class SpringSecurityConfiguration {

    AuthenticationManager authenticationManager;

    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService);
        authenticationManager = authenticationManagerBuilder.build();

        http.csrf().disable().cors().disable().authorizeHttpRequests().antMatchers("/api/v1/account/register", "/api/v1/account/auth").permitAll()
            .anyRequest().authenticated()
            .and()
            .authenticationManager(authenticationManager)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();}

}

这也是一种方法。

咱们来看一个具体的例子。

首先咱们新建一个 Spring Boot 工程,引入 Web 和 Spring Security 依赖,留神 Spring Boot 抉择最新版。

接下来咱们提供一个简略的测试接口,如下:

@RestController
public class HelloController {@GetMapping("/hello")
    public String hello() {return "hello 江南一点雨!";}
}

小伙伴们晓得,在 Spring Security 中,默认状况下,只有增加了依赖,咱们我的项目的所有接口就曾经被通通爱护起来了,当初启动我的项目,拜访 /hello 接口,就须要登录之后才能够拜访,登录的用户名是 user,明码则是随机生成的,在我的项目的启动日志中。

当初咱们的第一个需要是应用自定义的用户,而不是零碎默认提供的,这个简略,咱们只须要向 Spring 容器中注册一个 UserDetailsService 的实例即可,像上面这样:

@Configuration
public class SecurityConfig {

    @Bean
    UserDetailsService userDetailsService() {InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        users.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("admin").build());
        return users;
    }

}

这就能够了。

当然我当初的用户是存在内存中的,如果你的用户是存在数据库中,那么只须要提供 UserDetailsService 接口的实现类并注入 Spring 容器即可,这个之前在 vhr 视频中讲过屡次了(公号后盾回复 666 有视频介绍),这里就不再赘述了。

然而如果说我心愿 /hello 这个接口可能匿名拜访,并且我心愿这个匿名拜访还不通过 Spring Security 过滤器链,要是在以前,咱们能够重写 configure(WebSecurity) 办法进行配置,然而当初,得换一种玩法:

@Configuration
public class SecurityConfig {

    @Bean
    UserDetailsService userDetailsService() {InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        users.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("admin").build());
        return users;
    }

    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {web.ignoring().antMatchers("/hello");
            }
        };
    }

}

以前位于 configure(WebSecurity) 办法中的内容,当初位于 WebSecurityCustomizer Bean 中,该配置的货色写在这里就能够了。

那如果我还心愿对登录页面,参数等,进行定制呢?持续往下看:

@Configuration
public class SecurityConfig {

    @Bean
    UserDetailsService userDetailsService() {InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        users.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("admin").build());
        return users;
    }

    @Bean
    SecurityFilterChain securityFilterChain() {List<Filter> filters = new ArrayList<>();
        return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"), filters);
    }

}

Spring Security 的底层实际上就是一堆过滤器,所以咱们之前在 configure(HttpSecurity) 办法中的配置,实际上就是配置过滤器链。当初过滤器链的配置,咱们通过提供一个 SecurityFilterChain Bean 来配置过滤器链,SecurityFilterChain 是一个接口,这个接口只有一个实现类 DefaultSecurityFilterChain,构建 DefaultSecurityFilterChain 的第一个参数是拦挡规定,也就是哪些门路须要拦挡,第二个参数则是过滤器链,这里我给了一个空集合,也就是咱们的 Spring Security 会拦挡下所有的申请,而后在一个空集合中走一圈就完结了,相当于不拦挡任何申请。

此时重启我的项目,你会发现 /hello 也是能够间接拜访的,就是因为这个门路不通过任何过滤器。

其实我感觉目前这中新写法比以前老的写法更直观,更容易让大家了解到 Spring Security 底层的过滤器链工作机制。

有小伙伴会说,这写法跟我以前写的也不一样呀!这么配置,我也不晓得 Spring Security 中有哪些过滤器,其实,换一个写法,咱们就能够将这个配置成以前那种样子:

@Configuration
public class SecurityConfig {

    @Bean
    UserDetailsService userDetailsService() {InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        users.createUser(User.withUsername("江南一点雨").password("{noop}123").roles("admin").build());
        return users;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();}

}

这么写,就跟以前的写法其实没啥大的差异了。

2. 应用 Lambda

在最新版中,小伙伴们发现,很多常见的办法废除了,如下图:

包含大家相熟的用来连贯各个配置项的 and() 办法当初也废除了,并且依照官网的说法,将在 Spring Security7 中彻底移除该办法。

也就是说,你当前见不到相似上面这样的配置了:

@Override
protected void configure(HttpSecurity http) throws Exception {InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
    users.createUser(User.withUsername("javagirl").password("{noop}123").roles("admin").build());
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable()
            .userDetailsService(users);
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

and() 办法将被移除!

其实,松哥感觉移除 and 办法是个坏事,对于很多初学者来说,光是了解 and 这个办法就要良久。

从下面 and 办法的正文中小伙伴们能够看到,官网当初是在推动基于 Lambda 的配置来代替传统的链式配置,所以当前咱们的写法就得改成上面这样啦:

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.requestMatchers("/hello").hasAuthority("user").anyRequest().authenticated())
                .formLogin(form -> form.loginProcessingUrl("/login").usernameParameter("name").passwordParameter("passwd"))
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.maximumSessions(1).maxSessionsPreventsLogin(true));
        return http.build();}
}

其实,这里的几个办法倒不是啥新办法,只不过有的小伙伴可能之前不太习惯用下面这几个办法进行配置,习惯于链式配置。可是往后,就得缓缓习惯下面这种依照 Lambda 的形式来配置了,配置的内容倒很好了解,我感觉没啥好解释的。

3. 自定义 JSON 登录

自定义 JSON 登录也和之前旧版不太一样了。

3.1 自定义 JSON 登录

小伙伴们晓得,Spring Security 中默认的登录接口数据格式是 key-value 的模式,如果咱们想应用 JSON 格局来登录,那么就必须自定义过滤器或者自定义登录接口,上面松哥先来和小伙伴们展现一下这两种不同的登录模式。

3.1.1 自定义登录过滤器

Spring Security 默认解决登录数据的过滤器是 UsernamePasswordAuthenticationFilter,在这个过滤器中,零碎会通过 request.getParameter(this.passwordParameter) 的形式将用户名和明码读取进去,很显著这就要求前端传递参数的模式是 key-value。

如果想要应用 JSON 格局的参数登录,那么就须要从这个中央做文章了,咱们自定义的过滤器如下:

public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 获取申请头,据此判断申请参数类型
        String contentType = request.getContentType();
        if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {
            // 阐明申请参数是 JSON
            if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod());
            }
            String username = null;
            String password = null;
            try {
                // 解析申请体中的 JSON 参数
                User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
                username = user.getUsername();
                username = (username != null) ? username.trim() : "";
                password = user.getPassword();
                password = (password != null) ? password : "";
            } catch (IOException e) {throw new RuntimeException(e);
            }
            // 构建登录令牌
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                    password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            // 执行真正的登录操作
            Authentication auth = this.getAuthenticationManager().authenticate(authRequest);
            return auth;
        } else {return super.attemptAuthentication(request, response);
        }
    }
}

看过松哥之前的 Spring Security 系列文章的小伙伴,这段代码应该都是十分相熟了。

  1. 首先咱们获取申请头,依据申请头的类型来判断申请参数的格局。
  2. 如果是 JSON 格局的参数,就在 if 中进行解决,否则阐明是 key-value 模式的参数,那么咱们就调用父类的办法进行解决即可。
  3. JSON 格局的参数的解决逻辑和 key-value 的解决逻辑是统一的,惟一不同的是参数的提取形式不同而已。

最初,咱们还须要对这个过滤器进行配置:

@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

    @Bean
    JsonLoginFilter jsonLoginFilter() {JsonLoginFilter filter = new JsonLoginFilter();
        filter.setAuthenticationSuccessHandler((req,resp,auth)->{resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            // 获取以后登录胜利的用户对象
            User user = (User) auth.getPrincipal();
            user.setPassword(null);
            RespBean respBean = RespBean.ok("登录胜利", user);
            out.write(new ObjectMapper().writeValueAsString(respBean));
        });
        filter.setAuthenticationFailureHandler((req,resp,e)->{resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            RespBean respBean = RespBean.error("登录失败");
            if (e instanceof BadCredentialsException) {respBean.setMessage("用户名或者明码输出谬误,登录失败");
            } else if (e instanceof DisabledException) {respBean.setMessage("账户被禁用,登录失败");
            } else if (e instanceof CredentialsExpiredException) {respBean.setMessage("明码过期,登录失败");
            } else if (e instanceof AccountExpiredException) {respBean.setMessage("账户过期,登录失败");
            } else if (e instanceof LockedException) {respBean.setMessage("账户被锁定,登录失败");
            }
            out.write(new ObjectMapper().writeValueAsString(respBean));
        });
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/login");
        return filter;
    }

    @Bean
    AuthenticationManager authenticationManager() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager pm = new ProviderManager(daoAuthenticationProvider);
        return pm;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 开启过滤器的配置
        http.authorizeHttpRequests()
                // 任意申请,都要认证之后能力拜访
                .anyRequest().authenticated()
                .and()
                // 开启表单登录,开启之后,就会主动配置登录页面、登录接口等信息
                .formLogin()
                // 和登录相干的 URL 地址都放行
                .permitAll()
                .and()
                // 敞开 csrf 爱护机制,实质上就是从 Spring Security 过滤器链中移除了 CsrfFilter
                .csrf().disable();
        http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();}

}

这里就是配置一个 JsonLoginFilter 的 Bean,并将之增加到 Spring Security 过滤器链中即可。

在 Spring Boot3 之前(Spring Security6 之前),下面这段代码就能够实现 JSON 登录了。

然而从 Spring Boot3 开始,这段代码有点瑕疵了,间接用曾经无奈实现 JSON 登录了,具体起因松哥下文剖析。

3.1.2 自定义登录接口

另外一种自定义 JSON 登录的形式是间接自定义登录接口,如下:

@RestController
public class LoginController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/doLogin")
    public String doLogin(@RequestBody User user) {UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
        try {Authentication authenticate = authenticationManager.authenticate(unauthenticated);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            return "success";
        } catch (AuthenticationException e) {return "error:" + e.getMessage();
        }
    }
}

这里间接自定义登录接口,申请参数通过 JSON 的模式来传递。拿到用户名明码之后,调用 AuthenticationManager#authenticate 办法进行认证即可。认证胜利之后,将认证后的用户信息存入到 SecurityContextHolder 中。

最初再配一下登录接口就行了:

@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

    @Bean
    AuthenticationManager authenticationManager() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userService);
        ProviderManager pm = new ProviderManager(provider);
        return pm;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests()
                // 示意 /doLogin 这个地址能够不必登录间接拜访
                .requestMatchers("/doLogin").permitAll()
                .anyRequest().authenticated().and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();}
}

这也算是一种应用 JSON 格局参数的计划。在 Spring Boot3 之前(Spring Security6 之前),下面这个计划也是没有任何问题的。

从 Spring Boot3(Spring Security6)开始,下面这两种计划都呈现了一些瑕疵。

具体表现就是: 当你调用登录接口登录胜利之后,再去拜访零碎中的其余页面,又会跳转回登录页面,阐明拜访登录之外的其余接口时,零碎不晓得你曾经登录过了。

3.2 起因剖析

产生下面问题的起因,次要在于 Spring Security 过滤器链中有一个过滤器发生变化了:

在 Spring Boot3 之前,Spring Security 过滤器链中有一个名为 SecurityContextPersistenceFilter 的过滤器,这个过滤器在 Spring Boot2.7.x 中废除了,然而还在应用,在 Spring Boot3 中则被从 Spring Security 过滤器链中移除了,取而代之的是一个名为 SecurityContextHolderFilter 的过滤器。

在第一大节和小伙伴们介绍的两种 JSON 登录计划在 Spring Boot2.x 中能够运行在 Spring Boot3.x 中无奈运行,就是因为这个过滤器的变动导致的。

所以接下来咱们就来剖析一下这两个过滤器到底有哪些区别。

先来看 SecurityContextPersistenceFilter 的外围逻辑:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
    SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
    try {SecurityContextHolder.setContext(contextBeforeChainExecution);
        chain.doFilter(holder.getRequest(), holder.getResponse());
    }
    finally {SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        SecurityContextHolder.clearContext();
        this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
    }
}

我这里只贴出来了一些要害的外围代码:

  1. 首先,这个过滤器位于整个 Spring Security 过滤器链的第三个,是十分靠前的。
  2. 当登录申请通过这个过滤器的时候,首先会尝试从 SecurityContextRepository(上文中的 this.repo)中读取到 SecurityContext 对象,这个对象中保留了以后用户的信息,第一次登录的时候,这里实际上读取不到任何用户信息。
  3. 将读取到的 SecurityContext 存入到 SecurityContextHolder 中,默认状况下,SecurityContextHolder 中通过 ThreadLocal 来保留 SecurityContext 对象,也就是以后申请在后续的解决流程中,只有在同一个线程里,都能够间接从 SecurityContextHolder 中提取到以后登录用户信息。
  4. 申请持续向后执行。
  5. 在 finally 代码块中,以后申请曾经完结了,此时再次获取到 SecurityContext,并清空 SecurityContextHolder 避免内存透露,而后调用 this.repo.saveContext 办法保留以后登录用户对象(实际上是保留到 HttpSession 中)。
  6. 当前其余申请达到的时候,执行后面第 2 步的时候,就读取到以后用户的信息了,在申请后续的处理过程中,Spring Security 须要晓得以后用户的时候,会主动去 SecurityContextHolder 中读取以后用户信息。

这就是 Spring Security 认证的一个大抵流程。

然而,到了 Spring Boot3 之后,这个过滤器被 SecurityContextHolderFilter 取代了,咱们来看下 SecurityContextHolderFilter 过滤器的一个要害逻辑:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
    try {this.securityContextHolderStrategy.setDeferredContext(deferredContext);
        chain.doFilter(request, response);
    }
    finally {this.securityContextHolderStrategy.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

小伙伴们看到,后面的逻辑基本上还是一样的,不一样的是 finally 中的代码,finally 中少了一步向 HttpSession 保留 SecurityContext 的操作。

这下就明确了,用户登录胜利之后,用户信息没有保留到 HttpSession,导致下一次申请达到的时候,无奈从 HttpSession 中读取到 SecurityContext 存到 SecurityContextHolder 中,在后续的执行过程中,Spring Security 就会认为以后用户没有登录。

这就是问题的起因!

找到起因,那么问题就好解决了。

3.3 问题解决

首先问题出在了过滤器上,间接改过滤器倒也不是不能够,然而,既然 Spring Security 在降级的过程中摈弃了之前旧的计划,咱们又吃力的把之前旧的计划写回来,如同也不合理。

其实,Spring Security 提供了另外一个批改的入口,在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication 办法中,源码如下:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authResult);
    this.securityContextHolderStrategy.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

这个办法是以后用户登录胜利之后的回调办法,小伙伴们看到,在这个回调办法中,有一句 this.securityContextRepository.saveContext(context, request, response);,这就示意将以后登录胜利的用户信息存入到 HttpSession 中。

在以后过滤器中,securityContextRepository 的类型是 RequestAttributeSecurityContextRepository,这个示意将 SecurityContext 存入到以后申请的属性中,那很显著,在以后申请完结之后,这个数据就没了。在 Spring Security 的自动化配置类中,将 securityContextRepository 属性指向了 DelegatingSecurityContextRepository,这是一个代理的存储器,代理的对象是 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository,所以在默认的状况下,用户登录胜利之后,在这里就把登录用户数据存入到 HttpSessionSecurityContextRepository 中了。

当咱们自定义了登录过滤器之后,就毁坏了自动化配置里的计划了,这里应用的 securityContextRepository 对象就真的是 RequestAttributeSecurityContextRepository 了,所以就导致用户后续拜访时零碎认为用户未登录。

那么解决方案很简略,咱们只须要为自定义的过滤器指定 securityContextRepository 属性的值就能够了,如下:

@Bean
JsonLoginFilter jsonLoginFilter() {JsonLoginFilter filter = new JsonLoginFilter();
    filter.setAuthenticationSuccessHandler((req,resp,auth)->{resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        // 获取以后登录胜利的用户对象
        User user = (User) auth.getPrincipal();
          user.setPassword(null);
        RespBean respBean = RespBean.ok("登录胜利", user);
        out.write(new ObjectMapper().writeValueAsString(respBean));
    });
    filter.setAuthenticationFailureHandler((req,resp,e)->{resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        RespBean respBean = RespBean.error("登录失败");
        if (e instanceof BadCredentialsException) {respBean.setMessage("用户名或者明码输出谬误,登录失败");
        } else if (e instanceof DisabledException) {respBean.setMessage("账户被禁用,登录失败");
        } else if (e instanceof CredentialsExpiredException) {respBean.setMessage("明码过期,登录失败");
        } else if (e instanceof AccountExpiredException) {respBean.setMessage("账户过期,登录失败");
        } else if (e instanceof LockedException) {respBean.setMessage("账户被锁定,登录失败");
        }
        out.write(new ObjectMapper().writeValueAsString(respBean));
    });
    filter.setAuthenticationManager(authenticationManager());
    filter.setFilterProcessesUrl("/login");
    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
    return filter;
}

小伙伴们看到,最初调用 setSecurityContextRepository 办法设置一下就行。

Spring Boot3.x 之前之所以不必设置这个属性,是因为这里尽管没保留最初还是在 SecurityContextPersistenceFilter 过滤器中保留了。

那么对于自定义登录接口的问题,解决思路也是相似的:

@RestController
public class LoginController {

    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping("/doLogin")
    public String doLogin(@RequestBody User user, HttpSession session) {UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
        try {Authentication authenticate = authenticationManager.authenticate(unauthenticated);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
            return "success";
        } catch (AuthenticationException e) {return "error:" + e.getMessage();
        }
    }
}

小伙伴们看到,在登录胜利之后,开发者本人手动将数据存入到 HttpSession 中,这样就能确保下个申请达到的时候,可能从 HttpSession 中读取到无效的数据存入到 SecurityContextHolder 中了。

好啦,Spring Boot 新旧版本交替中,一个小小的问题,心愿小伙伴们可能有所播种。

正文完
 0