Spring Security解析九:AuthenticationManager

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security解析九:AuthenticationManager相关的知识,希望对你有一定的参考价值。

参考技术A

在Spring Security中对用户进行认证的是AuthenticationManager,其只有一个方法,尝试对封装了认证信息的Authentication进行身份验证,如果成功,则返回完全填充的Authentication(包括授予的权限)。

AuthenticationManager 只关注认证成功与否而并不关心具体的认证方式。例如我们可以通过用户名及密码、短信、刷脸、OAuth2协议等方式进行认证。对于这些具体认证方式是交给了AuthenticationProvider来负责。

下面展示了AuthenticationProvider的部分实现

AuthenticationManager与AuthenticationProvider 是怎么被创建使用,以及如何自由的添加认证方式的问题,在下面的内容中将进一步分析

前面章节分析了WebSecurityConfiguration配置类里面相关的内容,现在回到@EnableWebSecurity 注解上,我们接着分析下@EnableGlobalAuthentication内部都做了些啥

其重点就是导入了AuthenticationConfiguration配置对象

AuthenticationConfiguration 中还导入了ObjectPostProcessorConfiguration配置,该配置比较简单,就是实例化了一个bean,而该Bean在前面的章节中也在不断的用到

下面,我们深入分析下AuthenticationConfiguration配置类的实现。

先简单的说下AuthenticationManager构建的主体过程

由上门可知,其默认使用DefaultPasswordEncoderAuthenticationManagerBuilder作为认证管理的构建器,下面分析其build()方法的执行过程。

DefaultPasswordEncoderAuthenticationManagerBuilder在执行build()方法时,其父类AbstractConfiguredSecurityBuilder的doBuild()方法被执行,前面有说过,这个方法是个模板方法,如下所示:

接着我们分析下这三个默认的GlobalAuthenticationConfigurerAdapter类型的实例中init和configure方法都做了啥

该类的目的纯粹是为了添加InitializeUserDetailsManagerConfigurer配置,通过在其configure方法阶段创建DaoAuthenticationProvider对象,最终被添加到ProviderManager中

Springboot的自动化配置中会默认创建InMemoryUserDetailsManager,请参考 Spring Security解析二:自动化装配

我们也可以通过配置来指定,例如:

接着进一步研究下DaoAuthenticationProvider都做了些啥,它是怎么对身份进行认证的?

可见上的操作主要是从某个地方得到用户信息,然后检查用户的状态,如果检查失败则抛出相应的异常,否则返回成功的认证信息。
上面的retrieveUser与additionalAuthenticationChecks是需要继续研究的地方

上面通过userDetailsService来得到用户的信息,并通过passwordEncoder来验证密码是否正确,而这两个对象是通过上面 3.2小结里的InitializeUserDetailsManagerConfigurer中从ApplicationContext获得。

该类的目的纯粹是为了添加InitializeUserDetailsManagerConfigurer配置,通过在其configure方法阶段从ApplicationContext中得到AuthenticationProvider类型的Bean,并加入到ProviderManager中

小结:

经过上来的步骤后,在DefaultPasswordEncoderAuthenticationManagerBuilder的authenticationProviders属性中添加了一个或多个AuthenticationProvider,接下来的工作便是执行DefaultPasswordEncoderAuthenticationManagerBuilder的performBuild()方法完成AuthenticationManager的创建工作。当然,其实该方法是在父类AuthenticationManagerBuilder中的。

返回的其实是ProviderManager,而ProviderManager可以看成是AuthenticationManager的代理对象,里面保存了多个AuthenticationManager的实现。

Spring Security默认情况下为我们创建了一个基于用户名和密码进行验证的AuthenticationManager实例,同时收集ApplicationContext中的AuthenticationProvider类型的Bean 一起添加到ProviderManager(AuthenticationManager的子类)中供需要的地方进行使用。

Spring security UsernamePasswordAuthenticationToken 始终返回 403:用户凭据已过期

【中文标题】Spring security UsernamePasswordAuthenticationToken 始终返回 403:用户凭据已过期【英文标题】:Spring security UsernamePasswordAuthenticationToken always returns 403: User credentials have expired 【发布时间】:2021-07-06 14:51:10 【问题描述】:

我正在尝试在我的 kotlin 应用程序中实现基于 Spring 安全 JWT 的授权,但在每次 /authenticate(登录)时,UsernamePasswordAuthenticationToken 都会在数据库中找到用户,但返回 403:用户凭据已过期,即使对于刚刚添加的用户也是如此。

SecurityConfiguration 类:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
class SecurityConfiguration(
private val userDetailsService: UserDetailService,
private val jwtAuthorizationFilter: JwtAuthorizationFilter,
) : WebSecurityConfigurerAdapter() 

@Throws(Exception::class)
override fun configure(http: HttpSecurity) 
    http.cors().and()
        .csrf().disable()
         http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter::class.java)
        .authorizeRequests()
        .antMatchers("/api/public/**").permitAll()
        .antMatchers("/api/authenticate").permitAll()
        .anyRequest().authenticated()
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)


@Bean
@Throws(Exception::class)
override fun authenticationManagerBean(): AuthenticationManager 
    return super.authenticationManagerBean()


@Throws(Exception::class)
public override fun configure(auth: AuthenticationManagerBuilder) 
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())


@Bean
fun passwordEncoder() = BCryptPasswordEncoder()

@Bean
fun corsConfigurationSource(): CorsConfigurationSource 
    val source = UrlBasedCorsConfigurationSource()
    source.registerCorsConfiguration("/**", CorsConfiguration().applyPermitDefaultValues())
    return source
 

AuthorizationFilter 类

@Component
class JwtAuthorizationFilter(
private val jwtUtil: JtwUtil,
private val userDetailsService: UserDetailsService) : OncePerRequestFilter() 

@Throws(IOException::class, ServletException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse,
                              filterChain: FilterChain
) 
    val header = request.getHeader(SecurityConstants.TOKEN_HEADER)

    if (StringUtils.isEmpty(header) || !header.startsWith(SecurityConstants.TOKEN_PREFIX)) 
        filterChain.doFilter(request, response)
        return
    

    val jwt: String = header.substring(7)
    val username: String? = jwtUtil.extractUsername(jwt)

    if (username != null && SecurityContextHolder.getContext().authentication == null) 
        val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)
        val isValidToken: Boolean = jwtUtil.validateToken(jwt, userDetails)
        if (isValidToken) 
            val usernamePasswordAuthenticationToken = 
    UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
            usernamePasswordAuthenticationToken.details = 
    WebAuthenticationDetailsSource().buildDetails(request)
            SecurityContextHolder.getContext().authentication = 
    usernamePasswordAuthenticationToken
        
    

    filterChain.doFilter(request, response)
 

用户详细信息服务:

@Service
class UserDetailService(private val userRepository: UserRepository): UserDetailsService 
override fun loadUserByUsername(username: String?): UserDetails? 
    try 
        val user: UserTable = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("Username $username not found")
        return AppUserDetails(user)

     catch (e: Exception) 
        throw Exception(e)
    

这是我调用的 API 来尝试授权数据库中的用户

@RequestMapping("/api/authenticate")
@RestController
class AuthenticationController(
private val authenticationManager: AuthenticationManager,
private val userDetailsService: UserDetailsService,
private val userDetailService: UserDetailService,
private val jwtUtil: JtwUtil
) 

@PostMapping
fun authenticateUser(@RequestBody authenticationRequest: AuthenticationRequest): 
 ResponseEntity<AuthenticationResponse> 
    try 
        
 authenticationManager.authenticate
 (UsernamePasswordAuthenticationToken(authenticationRequest.username, 
 authenticationRequest.password))

        val userDetails: UserDetails = 
 userDetailsService.loadUserByUsername(authenticationRequest.username)
        val jwt: String = jwtUtil.generateToken(userDetails)

        return ResponseEntity.ok(AuthenticationResponse(jwt))
     catch (e: BadCredentialsException) 
        throw BadCredentialsException("Incorrect username or password $e")
    

我调试了请求,发现这个方法认证失败:

【问题讨论】:

【参考方案1】:

发现问题,可能对其他人有帮助。在我映射用户的 UserDetails 服务中,我将方法硬编码为始终返回 false

  override fun isCredentialsNonExpired(): Boolean 
    return false

【讨论】:

以上是关于Spring Security解析九:AuthenticationManager的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 入门(1-11)Spring Security - 匿名认证

Spring security UsernamePasswordAuthenticationToken 始终返回 403:用户凭据已过期

防止 Spring Security 通过下一个身份验证提供程序对具有 BadCredentialException 的用户进行身份验证

Spring Security 解析 —— Spring Security Oauth2 源码解析

spring-security.xml 文件的解析错误

Spring Security解析五:WebSecurityConfigurerAdapter