本文默认采用 Spring Boot。
Spring Boot 项目配置 Spring Security 需要写一个 Configuration 类继承 WebSecurityConfigurerAdapter 接口。
import org.springframework.security.config.web.servlet.invoke
@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
authorizeRequests {
authorize("/article", hasAuthority("read"))
authorize("/product", hasAuthority("read"))
}
formLogin {
failureUrl = "/403"
}
csrf {
val i = HandlerMappingIntrospector()
val r = MvcRequestMatcher(i, "/hello")
ignoringRequestMatchers(r)
}
cors {
configurationSource = CorsConfigurationSource {
val config = CorsConfiguration()
config.allowedOrigins = listOf("127.0.0.1:8080")
config.allowedMethods = listOf("GET", "POST")
return@CorsConfigurationSource config
}
}
addFilterBefore(AuthenticationLoggingFilter(), BasicAuthenticationFilter::class.java)
}
}
@Bean
public override fun userDetailsService(): UserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(userDetails)
}
}
这里 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
Spring MVC中Servlet是一个DispatcherServlet的实例。每个请求顶多被一个Servlet处理,但是每个请求可以被多个filter处理。filter可以打断处理流程,也可以修改request和response。
Spring提供了一个DelegatingFilterProxy的中间件。这个中间件可以让一个Spring Bean来代为该中间件的处理逻辑。它充当了Spring的ApplicationContext和Servlet生命周期之间的桥梁。
Spring Security提供了一个FilterChainProxy的Bean,让Filter的逻辑由SecurityFilterChain代为实现。
SecurityFilterChain中包含了所有的Spring Security提供的Filter,你还可以在SecurityFilterChain中插入你自定义的filter。
将filter注册在SecurityFilterChain上比注册到FilterChain上好很多。
- FilterChainProxy会自动清除SecurityContextHolder,避免内存泄漏。
- Spring Security提供了很多灵活性,Servlet默认只能根据URL去判断是否处理,而Spring Security则允许你编写自定义逻辑判断。
SecurityContextHolder中存放的SecurityContext是整条处理链共享的对象。里面存放了Authentication对象,最常见的Authentication对象是UsernamePasswordAuthentication对象。Principal存放着UserDetail对象,Credentials放着密码,Authorities是一个GrantedAuthority列表,由认证中间件配置。
自定义认证逻辑
可以看到认证的总体逻辑是这样的。 开发者在AuthenticationManager里做文章实现自定义认证逻辑
AuthenticationManager是一个接口,实现类一般是ProviderManager, ProviderManager中包含多个 AuthenticationProvider
因此我们要实现自定义的认证逻辑,就编写一个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可以用于
- 表单登陆Form Login
- Http Basic HTTP基础认证
- 摘要认证(已废弃)
自定义 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();
}
例子
感想
用 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