新版Spring Security6.2案例 - Authentication用户名密码
作者:mmseoamin日期:2024-02-02

前言:

前面有翻译了新版Spring Security6.2架构,包括总体架构,Authentication和Authorization,感兴趣可以直接点链接,这篇翻译官网给出的关于Authentication的Username/Password这页。

首先呢,官网就直接给出了基于用户名和密码的认证的代码,可以说是spring security的一个入门小案例,表单登录,输入用户名密码,和内存中的用户名密码匹配,如果匹配了就会成功登录。

Username/Password Authentication

验证用户的最常用方法之一是验证用户名和密码。Spring Security为使用用户名和密码进行身份验证提供了全面的支持。可以通过以下方式配置用户名密码认证

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.httpBasic(Customizer.withDefaults())
			.formLogin(Customizer.withDefaults());
		return http.build();
	}
	@Bean
	public UserDetailsService userDetailsService() {
		UserDetails userDetails = User.withDefaultPasswordEncoder()
			.username("user")
			.password("password")
			.roles("USER")
			.build();
		return new InMemoryUserDetailsManager(userDetails);
	}
}

前面的配置会自动向 SecurityFilterChain 注册内存中 UserDetailsService,将DaoAuthenticationProvider 注册到默认的 AuthenticationManager,并启用表单登录和 HTTP 基本身份验证。

官网这边也列举了很多例子,我这边也都附上了超链接,因为前面代码是表单登录的,所以优先把超链接第一点,"我想了解表单登录如何工作"这节翻译了。

要了解更多关于usernamepassword身份验证的信息,请考虑以下用例:

  • 我想了解表单登录如何工作
  • 我想了解HTTP基本身份验证如何工作
  • 我想了解DaoAuthenticationProvider如何工作
  • 我想在内存中管理用户
  • 我想管理数据库中的用户
  • 我想在LDAP中管理用户
  • 我想发布一个用于自定义身份验证的AuthenticationManager bean
  • 我想自定义全局AuthenticationManager

    表单登录的原理

    Spring Security支持通过HTML表单提供用户名和密码。首先,我们看看如何将用户重定向到登录表单

    新版Spring Security6.2案例 - Authentication用户名密码,第1张

    上图基于 SecurityFilterChain 流程图。 

    1. 首先,用户向未授权的资源 (/private) 发出未经身份验证的请求。
    2. Spring Security 的 AuthorizationFilter 通过抛出 AccessDeniedException 来指示未经身份验证的请求被拒绝。
    3. 由于用户未经过身份验证,因此 ExceptionTranslationFilter 将启动“启动身份验证”,并使用配置的 AuthenticationEntryPoint 将重定向发送到登录页。在大多数情况下,AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的实例。
    4. 浏览器请求重定向到的登录页面。
    5. 应用程序中的某些内容必须呈现在登录页面。

    当提交用户名和密码时,UsernamePasswordAuthenticationFilter对用户名和密码进行认证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,因此下面的图看起来应该非常相似。

    新版Spring Security6.2案例 - Authentication用户名密码,第2张

    该图建立在SecurityFilterChain图的基础上。

    1.当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter 通过从 HttpServletRequest 实例中提取用户名和密码来创建 UsernamePasswordAuthenticationToken,UsernamePasswordAuthenticationToken是一种身份验证类型。

    2.接下来,将UsernamePasswordAuthenticationToken传递到要进行身份验证的AuthenticationManager实例中。AuthenticationManager的细节取决于用户信息的存储方式。

    3.如果身份验证失败,则定义为”失败“,并做如下:

            (1).清除SecurityContextHolder。

            (2).调用 RememberMeServices.loginFail。如果未配置“记住我”,则为空操作。请参阅 Javadoc 中的 RememberMeServices 接口。

            (3).调用AuthenticationFailureHandler。请参阅Javadoc中的AuthenticationFailureHandler类

    4.如果身份验证成功,则显定义为”成功“,并做如下:

            (1).SessionAuthenticationStrategy收到新登录的通知。请参阅Javadoc中的SessionAuthenticationStrategy接口。

            (2).身份验证设置在securitycontexholder上。请参阅Javadoc中的SecurityContextPersistenceFilter类。

            (3).RememberMeServices。调用loginSuccess。如果记得我没有配置,这是一个no-op。请参阅Javadoc中的memormeservices接口。

            (4).ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。

            (5).调用AuthenticationSuccessHandler。通常,这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它会重定向到由ExceptionTranslationFilter保存的请求。

    默认情况下,Spring Security 表单登录处于启用状态。但是,一旦提供了任何基于 servlet 的配置,就必须显式提供基于表单的登录。以下示例显示了一个最小的显式 Java 配置:

    public SecurityFilterChain filterChain(HttpSecurity http) {
    	http
    		.formLogin(withDefaults());
    	// ...
    }

    在前面的配置中,Spring Security 呈现默认登录页面。大多数生产应用程序都需要自定义登录表单。

    以下配置演示了如何提供自定义登录表单。

    ublic SecurityFilterChain filterChain(HttpSecurity http) {
    	http
    		.formLogin(form -> form
    			.loginPage("/login")
    			.permitAll()
    		);
    	// ...
    }

    在 Spring Security 配置中指定登录页面时,用户负责呈现页面。以下 Thymeleaf 模板生成符合 /login 登录页的 HTML 登录表单。

    
    
    	
    		Please Log In
    	
    	
    		

    Please Log In

    Invalid username and password. You have been logged out.

    关于默认 HTML 表单,有几个关键点:

    • 表单应执行 post 到 /login。
    • 该表单需要包含一个 CSRF 令牌,该令牌由 Thymeleaf 自动包含。
    • 表单应在名为 username 的参数中指定用户名。
    • 表单应在名为 password 的参数中指定密码。
    • 如果找到名为 error 的 HTTP 参数,则表示用户未能提供有效的用户名或密码。
    • 如果找到名为 logout 的 HTTP 参数,则表示用户已成功注销。

      许多用户只需要自定义登录页面即可。但是,如果需要,您可以使用其他配置自定义前面显示的所有内容。

      如果您使用 Spring MVC,则需要一个将 GET /login 映射到我们创建的登录模板的控制器。以下示例显示了一个最小的 LoginController:

      @Controller
      class LoginController {
      	@GetMapping("/login")
      	String login() {
      		return "login";
      	}
      }

      最后在官网Username/Password这页中,还有描写关于自定义身份的authentication bean和全局authenticationManger,也就是上面超链接最后2点,先翻译出来,再后续博客中接着讲,本文可以着重看前面的表单登录即可。

      发布一个AuthenticationManager bean

      一个相当常见的要求是发布 AuthenticationManager bean 以允许自定义身份验证,例如在 @Service 或 Spring MVC @Controller中。例如,您可能希望通过 REST API 而不是使用表单登录对用户进行身份验证。

      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      	@Bean
      	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      		http
      			.authorizeHttpRequests((authorize) -> authorize
      				.requestMatchers("/login").permitAll()
      				.anyRequest().authenticated()
      			);
      		return http.build();
      	}
      	@Bean
      	public AuthenticationManager authenticationManager(
      			UserDetailsService userDetailsService,
      			PasswordEncoder passwordEncoder) {
      		DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
      		authenticationProvider.setUserDetailsService(userDetailsService);
      		authenticationProvider.setPasswordEncoder(passwordEncoder);
      		return new ProviderManager(authenticationProvider);
      	}
      	@Bean
      	public UserDetailsService userDetailsService() {
      		UserDetails userDetails = User.withDefaultPasswordEncoder()
      			.username("user")
      			.password("password")
      			.roles("USER")
      			.build();
      		return new InMemoryUserDetailsManager(userDetails);
      	}
      	@Bean
      	public PasswordEncoder passwordEncoder() {
      		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
      	}
      }

      有了上述配置,您就可以创建一个使用AuthenticationManager的@RestController,如下所示:

      @RestController
      public class LoginController {
      	private final AuthenticationManager authenticationManager;
      	public LoginController(AuthenticationManager authenticationManager) {
      		this.authenticationManager = authenticationManager;
      	}
      	@PostMapping("/login")
      	public ResponseEntity login(@RequestBody LoginRequest loginRequest) {
      		Authentication authenticationRequest =
      			UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
      		Authentication authenticationResponse =
      			this.authenticationManager.authenticate(authenticationRequest);
      		// ...
      	}
      	public record LoginRequest(String username, String password) {
      	}
      }

      本例中,如果需要,您有责任将经过身份验证的用户保存在securitycontextrerepository中。例如,如果使用HttpSession在请求之间持久化SecurityContext,您可以使用httpessionsecuritycontextrepository。

      自定义 AuthenticationManager

      通常,Spring Security 在内部构建一个 AuthenticationManager,该管理器由 DaoAuthenticationProvider 组成,用于用户名/密码身份验证。在某些情况下,可能仍需要自定义 Spring Security 使用的 AuthenticationManager 实例。例如,您可能需要简单地为缓存用户禁用凭据擦除。您可以使用以下配置发布 AuthenticationManager:

      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      	@Bean
      	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      		http
      			.authorizeHttpRequests((authorize) -> authorize
      				.requestMatchers("/login").permitAll()
      				.anyRequest().authenticated()
      			)
      			.httpBasic(Customizer.withDefaults())
      			.formLogin(Customizer.withDefaults());
      		return http.build();
      	}
      	@Bean
      	public AuthenticationManager authenticationManager(
      			UserDetailsService userDetailsService,
      			PasswordEncoder passwordEncoder) {
      		DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
      		authenticationProvider.setUserDetailsService(userDetailsService);
      		authenticationProvider.setPasswordEncoder(passwordEncoder);
      		ProviderManager providerManager = new ProviderManager(authenticationProvider);
      		providerManager.setEraseCredentialsAfterAuthentication(false);
      		return providerManager;
      	}
      	@Bean
      	public UserDetailsService userDetailsService() {
      		UserDetails userDetails = User.withDefaultPasswordEncoder()
      			.username("user")
      			.password("password")
      			.roles("USER")
      			.build();
      		return new InMemoryUserDetailsManager(userDetails);
      	}
      	@Bean
      	public PasswordEncoder passwordEncoder() {
      		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
      	}
      }

      或者,您可以利用用于构建 Spring Security 的全局 AuthenticationManager 的 AuthenticationManagerBuilder 作为 Bean 发布的事实。您可以按如下方式配置构建器:

      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      	@Bean
      	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      		// ...
      		return http.build();
      	}
      	@Bean
      	public UserDetailsService userDetailsService() {
      		// Return a UserDetailsService that caches users
      		// ...
      	}
      	@Autowired
      	public void configure(AuthenticationManagerBuilder builder) {
      		builder.eraseCredentials(false);
      	}
      }

      参考文献:

      《spring boot官网》