Spring Security架构
从官方这张图可以看出,Spring Security是通过内部的过滤器链实现认证和授权逻辑的。Spring Security内部的过滤器是有先后顺序的。比如UsernamePasswordAuthenticationFilter如果认证成功,那么AnonymousAuthenticationFilter肯定就不需要设置匿名者了。所以UsernamePasswordAuthenticationFilter在AnonymousAuthenticationFilter之前。具体顺序可看官网说明。
最后两个filter
??先说倒数第一个Filter:SecurityInterceptor。这个filter是最终做出抉择的地方,是通过还是拒绝会在filter中做出决定。如果通过则完成了Spring Security内部的过滤器链,继续执行web容器的其它过滤器。如果拒绝则会抛出异常。抛出的异常则会被前一个filter也就是ExceptionTranslationFilter拦截到。 ??ExceptionTranslationFilter就是倒数第二个filter。该filter的doFilter方法只是调用过滤器链的下一个filter也就是FilterSecurityInterceptor,但是会捕获所有异常,下面是简化版的异常处理方法,当然还有其它类型异常,Spring Security直接就抛出了不会做处理。
if (exception instanceof AuthenticationException) {
}else if (exception instanceof AccessDeniedException) {
}
可以看出就是针对认证异常和授权异常。对于这两者都有默认的处理器,当然Spring Security也提供了覆盖的方式。就是对于httpSecurity的配置时做如下的配置
httpSecurity.exceptionHandling()
.authenticationEntryPoint(xx)
.accessDeniedHandler(xx)
常用的UsernamePasswordAuthenticationFilter浅析
??当配置httpSecurity.loginForm的时候,就开启了UsernamePasswordAuthenticationFilter。但是否使用还要看登录url是否为该filter处理的url。默认配置为/login。当然也可以修改,就是使用如下配置指定。
httpSecurity.formLogin().loginProcessingUrl("xxx")
该filter默认就会执行配置的UserDetailsService的loadUserByUsername方法。该filter的dofilter在其父类中,下面是dofilter最后几句代码
catch (AuthenticationException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
可以看到认证失败则走unsuccessfulAuthentication处理。成功时如果continueChainBeforeSuccessfulAuthentication为true,才会继续走其它过滤器,而默认值为false。所以如果使用UsernamePasswordAuthenticationFilter处理登录,则默认是不走controller的,而是通过successfulAuthentication处理登录成功的响应。 为啥要这样设置呢?以下为个人见解:
-
我们知道filter的dofilter方法中chain.doFilter(request, response)后的代码是要等到servlet中的逻辑处理完成之后才会触发的。而在UserDetailsService的loadUserByUsername方法中已经完成了类似用户登录的校验及响应操作,所以没必要走controller层。 -
如果我们使用json格式提交,自定义一个类似UsernamePasswordAuthenticationFilter的filter,这里就叫JsonUsernamePasswordAuthenticationFilter。如果要提取用户名,则需要从request的输入流中读取,类似如下代码,然后使用json解析该字符串。
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
因为流只能读取一次,所以如果继续让代码走到controller层定义的登录接口,那么就会报出400Bad Request错误,因为request中已无数据。
- 没找到如何设置UsernamePasswordAuthenticationFilter的该参数为true,也没必要。(^o^)
successfulAuthentication方法内部的默认successHandler会将context保存到session中,如果重写了其中的successHandler也没用,因为SecurityContextPersistenceFilter会在dofilter最后再次执行一次保存到session中如果之前没有保存的话。如下所示。
try{
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
当其它请求过来时,SecurityContextPersistenceFilter这个过滤器会从session中取出context设置到SecurityContextHolder中。 当然如果关闭了session的话
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
SecurityContextPersistenceFilter中的repo变量类型会发生变化,所以不会存入到session中。
|