Spring 有两个安全配置 - 失败 API 登录重定向到表单登录页面。如何改变?
Spring with two security configurations - failed API login redirects to form login page. How to change?
我有一个 Spring 启动应用程序,它有两个安全配置(两个 WebSecurityConfigurerAdapter
s),一个用于带有“/api/**
”端点的 REST API,另一个对于所有其他端点的网络 front-end。安全配置是 here on Github,这里是它的一些相关部分:
@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
jwtAuthenticationFilter.setPostOnly(true);
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
@Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login").permitAll()
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/?logout")
.and()
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/home").authenticated()
.anyRequest().denyAll()
.and();
}
}
JWTAuthenticationFilter
是 UsernamePasswordAuthenticationFilter
的自定义子类,它处理 sign-in 对 REST API 的尝试(通过 HTTP POST 和 JSON body 到 /api/login
) 和 returns 如果成功,"Authorization" header 中的 JWT 令牌。
所以问题是: 尝试登录 /api/login
失败(凭据错误或丢失 JSON body)是 重定向到HTML登录表单/api/login
。 Non-authenticated 对其他“/api/**
”端点的请求导致简单的 JSON 响应,例如:
{
"timestamp": "2019-11-22T21:03:07.892+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/v1/agency"
}
{
"timestamp": "2019-11-22T21:04:46.663+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/v1/badlink"
}
尝试通过 non-authenticated 用户重定向到登录表单 /login
来访问其他受保护的 URL(不是以“/api/
”开头)期望的行为。但我不希望 API 调用 /api/login
重定向到该表单!
如何为失败的 API 登录编写正确的行为代码? 是否是为该过滤器添加新处理程序的问题?或者可能对我已经定义的某些行为添加排除项?
关于异常和处理的更多细节:
我查看了日志,由于凭据错误或格式错误 JSON 而引发的异常是 org.springframework.security.authentication.AuthenticationException
的子类。日志显示,例如:
webapp_1 | 2019-11-25 15:30:16.048 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
(...stack trace...)
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@7f9648b6
webapp_1 | 2019-11-25 15:30:16.133 DEBUG 1 --- [nio-8080-exec-6] n.j.webapps.granite.home.HomeController : Accessing /login page.
当我访问另一个 URL 时,例如不存在的 /api/x
,它看起来非常不同。它非常冗长,但看起来服务器正试图重定向到 /error
,但没有发现它是授权的 URL。有趣的是,如果我在 Web 浏览器中尝试此操作,我会收到使用我的自定义错误页面 (error.html) 格式化的错误,但如果我使用 Postman 访问它,我只会收到一条 JSON 消息。日志示例:
webapp_1 | 2019-11-25 16:07:22.157 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
webapp_1 |
webapp_1 | org.springframework.security.access.AccessDeniedException: Access is denied
...
webapp_1 | 2019-11-25 16:07:22.174 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Calling Authentication entry point.
webapp_1 | 2019-11-25 16:07:22.175 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
...
webapp_1 | 2019-11-25 16:07:22.211 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/error'; against '/api/**'
...
webapp_1 | 2019-11-25 16:07:22.214 DEBUG 1 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy : /error reached end of additional filter chain; proceeding with original chain
webapp_1 | 2019-11-25 16:07:22.226 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
webapp_1 | 2019-11-25 16:07:22.230 DEBUG 1 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
webapp_1 | 2019-11-25 16:07:22.564 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
webapp_1 | 2019-11-25 16:07:22.577 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Mon Nov 25 16:07:22 GMT 2019, status=403, error=Forbidden, message=Access Denied, path=/a (truncated)...]
webapp_1 | 2019-11-25 16:07:22.903 DEBUG 1 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
webapp_1 | 2019-11-25 16:07:22.905 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 403
所以看起来我可能需要做的是为 REST API 配置 "authentication failure handler" 以转到“/error”而不是转到“/login”,但仅对于 /api/**
.
下的端点
我将 unsuccessfulAuthentication()
的 @Override 添加到我的身份验证过滤器
祖父母 class (AbstractAuthenticationProcessingFilter
) 有一个用于不成功身份验证的方法(即 AuthenticationException
),该方法委托给身份验证失败处理程序 class。我本可以创建自己的自定义身份验证失败处理程序,但我决定简单地用一些代码覆盖 unsuccessfulAuthentication
方法,该代码发送回具有 401 状态和 JSON 错误消息的响应:
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"authentication error?\"}");
}
...并添加了自定义 AuthenticationEntryPoint
以使其他错误匹配
这没有我在其他端点看到的错误消息的确切形式,所以我还创建了一个自定义 AuthenticationEntryPoint
(class 处理对受保护端点的未授权请求)它基本上做同样的事情。我的实现:
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"unauthorized?\"}");
}
}
现在 REST 端点的安全配置如下所示(注意添加“.exceptionHandling().authenticationEntryPoint()
”:
@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint())
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
我需要处理我的错误消息,以便它们提供更多信息和安全。
这并不能完全回答我最初的问题,即如何获得我喜欢的那些 default-style JSON 响应,但它 确实 允许我自定义错误独立于 Web 配置的所有类型的访问失败。 (/api/**
之外的端点仍然可以使用 Web 表单登录。)
截至当前提交的完整代码:github
我有一个 Spring 启动应用程序,它有两个安全配置(两个 WebSecurityConfigurerAdapter
s),一个用于带有“/api/**
”端点的 REST API,另一个对于所有其他端点的网络 front-end。安全配置是 here on Github,这里是它的一些相关部分:
@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
jwtAuthenticationFilter.setPostOnly(true);
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
@Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login").permitAll()
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/?logout")
.and()
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/home").authenticated()
.anyRequest().denyAll()
.and();
}
}
JWTAuthenticationFilter
是 UsernamePasswordAuthenticationFilter
的自定义子类,它处理 sign-in 对 REST API 的尝试(通过 HTTP POST 和 JSON body 到 /api/login
) 和 returns 如果成功,"Authorization" header 中的 JWT 令牌。
所以问题是: 尝试登录 /api/login
失败(凭据错误或丢失 JSON body)是 重定向到HTML登录表单/api/login
。 Non-authenticated 对其他“/api/**
”端点的请求导致简单的 JSON 响应,例如:
{
"timestamp": "2019-11-22T21:03:07.892+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/v1/agency"
}
{
"timestamp": "2019-11-22T21:04:46.663+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/v1/badlink"
}
尝试通过 non-authenticated 用户重定向到登录表单 /login
来访问其他受保护的 URL(不是以“/api/
”开头)期望的行为。但我不希望 API 调用 /api/login
重定向到该表单!
如何为失败的 API 登录编写正确的行为代码? 是否是为该过滤器添加新处理程序的问题?或者可能对我已经定义的某些行为添加排除项?
关于异常和处理的更多细节:
我查看了日志,由于凭据错误或格式错误 JSON 而引发的异常是 org.springframework.security.authentication.AuthenticationException
的子类。日志显示,例如:
webapp_1 | 2019-11-25 15:30:16.048 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
(...stack trace...)
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@7f9648b6
webapp_1 | 2019-11-25 15:30:16.133 DEBUG 1 --- [nio-8080-exec-6] n.j.webapps.granite.home.HomeController : Accessing /login page.
当我访问另一个 URL 时,例如不存在的 /api/x
,它看起来非常不同。它非常冗长,但看起来服务器正试图重定向到 /error
,但没有发现它是授权的 URL。有趣的是,如果我在 Web 浏览器中尝试此操作,我会收到使用我的自定义错误页面 (error.html) 格式化的错误,但如果我使用 Postman 访问它,我只会收到一条 JSON 消息。日志示例:
webapp_1 | 2019-11-25 16:07:22.157 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
webapp_1 |
webapp_1 | org.springframework.security.access.AccessDeniedException: Access is denied
...
webapp_1 | 2019-11-25 16:07:22.174 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Calling Authentication entry point.
webapp_1 | 2019-11-25 16:07:22.175 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
...
webapp_1 | 2019-11-25 16:07:22.211 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/error'; against '/api/**'
...
webapp_1 | 2019-11-25 16:07:22.214 DEBUG 1 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy : /error reached end of additional filter chain; proceeding with original chain
webapp_1 | 2019-11-25 16:07:22.226 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
webapp_1 | 2019-11-25 16:07:22.230 DEBUG 1 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
webapp_1 | 2019-11-25 16:07:22.564 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
webapp_1 | 2019-11-25 16:07:22.577 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Mon Nov 25 16:07:22 GMT 2019, status=403, error=Forbidden, message=Access Denied, path=/a (truncated)...]
webapp_1 | 2019-11-25 16:07:22.903 DEBUG 1 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
webapp_1 | 2019-11-25 16:07:22.905 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 403
所以看起来我可能需要做的是为 REST API 配置 "authentication failure handler" 以转到“/error”而不是转到“/login”,但仅对于 /api/**
.
我将 unsuccessfulAuthentication()
的 @Override 添加到我的身份验证过滤器
祖父母 class (AbstractAuthenticationProcessingFilter
) 有一个用于不成功身份验证的方法(即 AuthenticationException
),该方法委托给身份验证失败处理程序 class。我本可以创建自己的自定义身份验证失败处理程序,但我决定简单地用一些代码覆盖 unsuccessfulAuthentication
方法,该代码发送回具有 401 状态和 JSON 错误消息的响应:
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"authentication error?\"}");
}
...并添加了自定义 AuthenticationEntryPoint
以使其他错误匹配
这没有我在其他端点看到的错误消息的确切形式,所以我还创建了一个自定义 AuthenticationEntryPoint
(class 处理对受保护端点的未授权请求)它基本上做同样的事情。我的实现:
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"unauthorized?\"}");
}
}
现在 REST 端点的安全配置如下所示(注意添加“.exceptionHandling().authenticationEntryPoint()
”:
@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint())
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
我需要处理我的错误消息,以便它们提供更多信息和安全。
这并不能完全回答我最初的问题,即如何获得我喜欢的那些 default-style JSON 响应,但它 确实 允许我自定义错误独立于 Web 配置的所有类型的访问失败。 (/api/**
之外的端点仍然可以使用 Web 表单登录。)
截至当前提交的完整代码:github