本文基于 spring boot starter security 2.2.1.RELEASE
不多BB,先看安全配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class CloudSimpleSecurityConfig extends WebSecurityConfigurerAdapter {
// .... 省略部分配置代码
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and();
}
}
这里没有开启formLogin功能,是个标准的Restful like 服务提供者。如果我们自定义了某些 Filter,在Filter里面提供了一些自定义的认证方式,然后抛出了一些自定义的异常,怎么返回给前端呢。先看 Filter
public class AuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws CustomeException { // 抛出自定义的业务异常
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String url = httpServletRequest.getRequestURI();
log.info("请求路径: {}", url);
// 从头中获取token信息并进行认证授权流程
attemptAuthentication(httpServletRequest, MicroConstants.AUTHORIZATION_HEADER);
filterChain.doFilter(servletRequest, servletResponse);
}
private void attemptAuthentication(HttpServletRequest request, String header){
// ....
setUserInfo("xxx");
}
public void setUserInfo(UserVO user){
// 这里将user 信息存入 Security 上下文中实现认证授权流程
}
}
追踪源码我们可以知道运行过程中的 Filter 链中有一个 ExceptionTranslationFilter 负责处理链中的异常信息的
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 只有当我们抛出来的异常是 AuthenticationException或AccessDeniedException 的子类的时候,才进这个处理流程
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
很明显,我们的自定义异常不可能是 AuthenticationException 或 AccessDeniedException 的子类的,那怎么办,继续往下看,如果都不是,他会直接 throw new RuntimeException(ex) , 最终会请求 /error 的地址,也就是springboot 默认的那个错误页面,如果不加这个Filter, 我们以往的处理逻辑是在服务内部定义一个全局的异常处理,加上 @RestControllerAdvice 注解就能生效了,像下面这样
@RestControllerAdvice
public class GlobalExceptionHandler {
/** 自定义业务异常捕获 */
@ExceptionHandler(CustomException.class)
public Result handleBusinessException(CustomException e){
log.error("storage 异常:", e);
return Result.error(e.getResultCode()); //自定义的异常返回结构,一般都是返回json,也可以是xml
}
}
但这里就不行了,因为这里抛出的异常是在 @RestControllerAdvice 生效的更前面。也就是还没进入它的处理范畴。所以我们可以这样
@RestController
@RequestMapping("/")
public class FallbackErrorController implements ErrorController {
public static final String PATH = "/error";
@Override
public String getErrorPath() {
return PATH;
}
@GetMapping(value = PATH)
public Result ok(HttpServletRequest request){
Throwable e = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
if(e instanceof AuthenticationException){
return Result.error(ResultCode.InvalidCredential);
}else if(e instanceof CustomException){
return Result.error(ResultCode.PermissionDenied);
}else {
return Result.error(ResultCode.ServiceError);
}
}
}
自己定义这个 /error 的 Request Handler, 到这里我们可以从request中捕捉这个异常,然后返回给前端。Over !