@[toc] Spring Security 在最近几个版别中装备的写法都有一些改变,很多常见的办法都抛弃了,而且将在未来的 Spring Security7 中移除,因而松哥在上一年旧文的根底之上,又补充了一些新的内容,重新发一下,供各位运用 Spring Security 的小伙伴们参阅。

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

1. WebSecurityConfigurerAdapter

Spring Security6 全新写法,大变样!

首要榜首点,便是各位小伙伴最简略发现的 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

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

Spring Security6 全新写法,大变样!

包括大家了解的用来连接各个装备项的 and() 办法现在也抛弃了,而且依照官方的说法,将在 Spring Security7 中彻底移除该办法。

Spring Security6 全新写法,大变样!

也便是说,你今后见不到相似下面这样的装备了:

@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 新旧版别交替中,一个小小的问题,期望小伙伴们可以有所收获。