本文已参与「新人创作礼」活动,一起开启掘金创作之路。
spring cloud security 是构建 spring cloud 微服务中可靠的安全管理模块,其中 HttpSecurity 配置可以定制每个应用自己的安全策略,在 HttpSecurity 配置中 authenticationEntryPoint 配置项是用于处理凭证错误或无对应权限访问的入口,这里探讨这个入口工作的过程。
配置
配置 authenticationEntryPoint 需要继承 WebSecurityConfigurerAdapter 类并重写 protected void configure(HttpSecurity httpSecurity) 方法
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class TheSecurityConfig extends WebSecurityConfigurerAdapter {
private EntryPoint entryPoint;
private AccessDenied accessDenied;
public TheSecurityConfig(EntryPoint entryPoint, AccessDenied accessDenied){
this.entryPoint = entryPoint;
this.accessDenied = accessDenied;
}
@Bean
public TokenAuthFilter tokenAuthFilter() throws Exception {
TokenAuthFilter tokenAuthFilter = new TokenAuthFilter(authenticationManager());
return tokenAuthFilter;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 拦截规则
.and()
.authorizeRequests()
.anyRequest().authenticated()
// 未授权处理
.and()
.exceptionHandling()
.authenticationEntryPoint(entryPoint)
.accessDeniedHandler(accessDenied)
.and()
// 自定义 token 解析过滤器获取权限和角色信息
.addFilter(tokenAuthFilter())
.csrf().disable();
}
}
以上配置采用自定义的认证接口,故没有配置 formLogin 项,其中 EntryPoint AccessDenied TokenAuthFilter可大致为以下形式
@Component
public class EntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 通过 response 写入返回内容
}
}
@Component
public class AccessDenied implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 通过 response 写入返回内容
}
}
public class TokenAuthFilter extends BasicAuthenticationFilter {
public TokenAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("Authorization");
UsernamePasswordAuthenticationToken authRequest = null;
// 自定义解析 token 将解析成功的信息存入 authRequest
// 形如 authRequest = new UsernamePasswordAuthenticationToken(username, token, authorities);
// authorities 是 Collection<GrantedAuthority> 类型,存入 SimpleGrantedAuthority 类型数据,是该请求的 角色信息或权限信息
if(authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
}
请求过滤过程
请求进入服务过程中,运行 ApplicationFilterChain 中的 internalDoFilter 方法,核心代码如下
private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
// ...
try {
// ...
if( Globals.IS_SECURITY_ENABLED ) {
// ...
} else {
// 运行过滤器
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
// 异常捕获与抛出
throw e;
} catch (Throwable e) {
// ...
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
// ...
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
// ...
} else {
// 过滤器运行完毕后进入服务处理
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
// 异常捕获与抛出
throw e;
} catch (Throwable e) {
// ...
} finally {
// ...
}
}
在 ExceptionTranslationFilter 这个过滤器中,存在一个异常捕获器
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// ...
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// ...
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// ...
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// ...
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
// ...
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
else {
// ...
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
// ...
// 这里就是配置的 authenticationEntryPoint 触发位置
this.authenticationEntryPoint.commence(request, response, reason);
}
通过以上源码可知,如果抛出 AccessDeniedException 且没有被 ExceptionTranslationFilter 之后的过滤器捕获并处理的话,将会到达该过滤器的 sendStartAuthentication 处理方法并调用 authenticationEntryPoint 方法,该方法就是配置 httpSecurity 中 authenticationEntryPoint 的入口方法,如果没有配置该方法时,请求返回内容将为空。
异常抛出
类 FilterOrderRegistration 中在 ExceptionTranslationFilter 后注册了 FilterSecurityInterceptor 过滤器,该过滤器继承了 AbstractSecurityInterceptor 类,在其 doFilter 方法中,调用了 invoke 方法,这个方法里有如下代码:
// 检查该请求是否可以被放行,不被放行便抛出 AccessDeniedException 异常
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
上述代码中如果抛出 AccessDeniedException 异常,因为没有捕获处理错误的方法,便会抛向上一个过滤器,如果我们不改变过滤器顺序的话,就会在 ExceptionTranslationFilter 中进行捕获和处理。
如果请求被放行,则 FilterSecurityInterceptor 中不会抛出异常,便会走到下一个过滤器直到 internalDoFilter 类中的 servlet.service(request, response); 方法。如果在请求的方法上加诸如 @PreAuthorize("hasAuthority('admin')") 的注解的话,如果没有 admin 权限,也会到 AbstractSecurityInterceptor 类中的 attemptAuthorization 方法中抛出 AccessDeniedException 错误,一样的,如果在 ExceptionTranslationFilter 之后的过滤器中没有捕获处理的话,也会走到 sendStartAuthentication 方法中进行处理。
自定义捕获过滤器
上文了解到,AccessDeniedException 会被 ExceptionTranslationFilter 捕获,但如果我们在 ExceptionTranslationFilter 过滤器后添加一个专门处理 AccessDeniedException 错误的过滤器,则不需要配置 AuthenticationEntryPoint 入口,这样的过滤器可以是如下形式:
public class SomeFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try{
chain.doFilter(request, response);
} catch (Exception e){
if(e instanceof AccessDeniedException){
// 通过 response 写入返回内容或其他处理
}
}
}
相应的, httpSecurity 需要添加 .addFilterAfter(SomeFilter(), ExceptionTranslationFilter.class) 配置。这样,就能不使用 AuthenticationEntryPoint 入口了,但这是没有必要的,这里还是建议使用 AuthenticationEntryPoint 入口配置。这里提出的只是一种参考方法,或者,需要注意不能在 ExceptionTranslationFilter 后添加一个捕获 AccessDeniedException 异常的过滤器,这会导致 AuthenticationEntryPoint 入口方法失效,如果捕获也应继续向上抛出。