本文默认采用 Spring Boot。
Spring Boot 项目配置 Spring Security 需要写一个 Configuration 类继承 WebSecurityConfigurerAdapter 接口。

  1. import org.springframework.security.config.web.servlet.invoke
  2. @Configuration
  3. class SecurityConfig : WebSecurityConfigurerAdapter() {
  4. override fun configure(http: HttpSecurity) {
  5. http {
  6. authorizeRequests {
  7. authorize("/article", hasAuthority("read"))
  8. authorize("/product", hasAuthority("read"))
  9. }
  10. formLogin {
  11. failureUrl = "/403"
  12. }
  13. csrf {
  14. val i = HandlerMappingIntrospector()
  15. val r = MvcRequestMatcher(i, "/hello")
  16. ignoringRequestMatchers(r)
  17. }
  18. cors {
  19. configurationSource = CorsConfigurationSource {
  20. val config = CorsConfiguration()
  21. config.allowedOrigins = listOf("127.0.0.1:8080")
  22. config.allowedMethods = listOf("GET", "POST")
  23. return@CorsConfigurationSource config
  24. }
  25. }
  26. addFilterBefore(AuthenticationLoggingFilter(), BasicAuthenticationFilter::class.java)
  27. }
  28. }
  29. @Bean
  30. public override fun userDetailsService(): UserDetailsService {
  31. val userDetails = User.withDefaultPasswordEncoder()
  32. .username("user")
  33. .password("password")
  34. .roles("USER")
  35. .build()
  36. return InMemoryUserDetailsManager(userDetails)
  37. }
  38. }

这里 http { } 稍显奇怪的语法是因为 Spring Security 提供了一个 DSL来配置 http: HttpSecurity 。实现同样的逻辑,不用DSL的话是这样的。

override fun configure(http: HttpSecurity) {
    http.authorizeRequests()
                ?.mvcMatchers("/article", "/product")
                ?.hasAuthority("read")
            http.httpBasic()
            http.formLogin()
                ?.failureForwardUrl("/403")
            http.csrf { c ->
                val i = HandlerMappingIntrospector()
                val r = MvcRequestMatcher(i, "/hello")
                c.ignoringRequestMatchers(r)
            }
            http.cors { c ->
                val source = CorsConfigurationSource { _ ->
                    val config = CorsConfiguration()
                    config.allowedOrigins = listOf("127.0.0.1:8080")
                    config.allowedMethods = listOf("GET", "POST")
                    return@CorsConfigurationSource config
                }
                c.configurationSource(source)
            }
            http.addFilterBefore(AuthenticationLoggingFilter(), BasicAuthenticationFilter::class.java)
}

虽然看上去凌乱但是会比 DSL 更灵活。

中间件

Spring Security 总体来说是围绕着中间件(filter)去设计的。
Servlet的filter
image.png
Spring MVC中Servlet是一个DispatcherServlet的实例。每个请求顶多被一个Servlet处理,但是每个请求可以被多个filter处理。filter可以打断处理流程,也可以修改request和response。
image.png
Spring提供了一个DelegatingFilterProxy的中间件。这个中间件可以让一个Spring Bean来代为该中间件的处理逻辑。它充当了Spring的ApplicationContext和Servlet生命周期之间的桥梁。
image.png
Spring Security提供了一个FilterChainProxy的Bean,让Filter的逻辑由SecurityFilterChain代为实现。
image.png
SecurityFilterChain中包含了所有的Spring Security提供的Filter,你还可以在SecurityFilterChain中插入你自定义的filter。
将filter注册在SecurityFilterChain上比注册到FilterChain上好很多。

  1. FilterChainProxy会自动清除SecurityContextHolder,避免内存泄漏。
  2. Spring Security提供了很多灵活性,Servlet默认只能根据URL去判断是否处理,而Spring Security则允许你编写自定义逻辑判断。

image.png
SecurityContextHolder中存放的SecurityContext是整条处理链共享的对象。里面存放了Authentication对象,最常见的Authentication对象是UsernamePasswordAuthentication对象。Principal存放着UserDetail对象,Credentials放着密码,Authorities是一个GrantedAuthority列表,由认证中间件配置。

自定义认证逻辑

image.png
可以看到认证的总体逻辑是这样的。 开发者在AuthenticationManager里做文章实现自定义认证逻辑
AuthenticationManager是一个接口,实现类一般是ProviderManager, ProviderManager中包含多个 AuthenticationProvider
image.png
因此我们要实现自定义的认证逻辑,就编写一个AuthenticationProvider的实现类,注册到AuthenticationManagerBuilder即可。
说到这是不是有点晕了呢?实际上操作起来很简单,先编写认证逻辑的实现类

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("john".equals(username) && "12345".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }

    /**
     * 只有当 Authentication 的类型是 UsernamePasswordAuthenticationToken 才执行这个类的认证逻辑
     **/
    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

然后注册到AuthenticationManagerBuilder

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authenticationProvider);
}

就完成了。
我们这样编写的AuthenticationProvider可以用于

  1. 表单登陆Form Login
  2. Http Basic HTTP基础认证
  3. 摘要认证(已废弃)

表单登陆的情况流程为
image.png

自定义 filter

Spring Security的自带filter是有顺序的。你可以选择将你自定义的filter插在某个filter之前或者之后。
ChannelProcessingFilter

  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

编写filter也非常简单,先编写一个filter继承OncePerRequestFilter

public class AuthenticationLoggingFilter extends OncePerRequestFilter {

    private final Logger logger =
            Logger.getLogger(AuthenticationLoggingFilter.class.getName());


    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String requestId = request.getHeader("Request-Id");

        logger.info("Successfully authenticated request with id " +  requestId);

        filterChain.doFilter(request, response);
    }
}

然后在configure中插入即可

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterAfter(
            new AuthenticationLoggingFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest()
                .permitAll();
}

例子

打印CSRF Token

感想

用 Kotlin DSL 配置 Spring Security 实际上完全不如 Java 直观,而且并没有带来多大好处。

参考文献

[1]: Spring Security in Action https://www.manning.com/books/spring-security-in-action
[2]: Spring Security in Action 的代码仓库 https://github.com/havinhphu188/spring-security-in-action-source/blob/master/ssia-ch2-ex3/src/main/java/com/laurentiuspilca/ssia/config/ProjectConfig.java
[3]: Spring Security Kotlin 官方范例 https://github.com/spring-projects/spring-security-samples/blob/main/servlet/spring-boot/kotlin/hello-security/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt
[4]: Spring Security 5.6.x 官方文档 https://docs.spring.io/spring-security/site/docs/5.6.x/reference/html5/#servlet-architecture