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
入口方法失效,如果捕获也应继续向上抛出。