[英]Spring with two security configurations - failed API login redirects to form login page. How to change?
I have a Spring Boot application with two security configurations (two WebSecurityConfigurerAdapter
s), one for a REST API with " /api/**
" endpoints, and one for a web front-end at all other endpoints. 我有一个具有两个安全配置(两个WebSecurityConfigurerAdapter
)的Spring Boot应用程序,一个用于具有“ /api/**
”端点的REST API,一个用于所有其他端点的Web前端。 The security configuration is here on Github and here's some relevant parts of it: 安全配置在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();
}
}
The JWTAuthenticationFilter
is a custom subclass of UsernamePasswordAuthenticationFilter
that processes sign-in attempts to the REST API (by HTTP POST with a JSON body to /api/login
) and returns a JWT token in the "Authorization" header if successful. JWTAuthenticationFilter
是UsernamePasswordAuthenticationFilter
的自定义子类,用于处理对REST API的登录尝试(通过HTTP POST,JSON主体为/api/login
),如果成功,则在“ Authorization”标头中返回JWT令牌。
So here's the issue: failed login attempts to /api/login
(either with bad credentials or missing JSON body) are redirecting to the HTML login form /api/login
. 所以这就是问题所在: /api/login
失败登录尝试(具有错误的凭据或缺少JSON正文)将重定向到HTML登录表单/api/login
。 Non-authenticated requests to other " /api/**
" endpoints result in a simple JSON response such as: 对其他“ /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"
}
Attempts to access other protected URLs (not starting with " /api/
") by a non-authenticated user redirect to the login form /login
, which is the desired behavior. 未经身份验证的用户尝试访问其他受保护的URL(不以“ /api/
开头”)重定向到登录表单/login
,这是所需的行为。 But I don't want API calls to /api/login
to redirect to that form! 但是我不希望对/api/login
API调用重定向到该表单!
How can I code the correct behavior for failed API logins? 如何为API登录失败编码正确的行为? Is it a question of adding a new handler for that filter? 这是为该过滤器添加新处理程序的问题吗? Or maybe adding an exclusion to some behavior I've already defined? 还是在我已经定义的某些行为中添加排除项?
I looked at the logs, and the exception being thrown for either bad credentials or malformed JSON is a subclass of org.springframework.security.authentication.AuthenticationException
. 我查看了日志,由于凭据错误或JSON格式错误而引发的异常是org.springframework.security.authentication.AuthenticationException
的子类。 The logs show, for example: 日志显示,例如:
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.
When I access another URL, for example one that doesn't exist such as /api/x
, it looks very different. 当我访问另一个URL时,例如一个不存在的URL,例如/api/x
,它看起来非常不同。 It's pretty verbose but it looks like the server is trying to redirect to /error
and is not finding that to be an authorized URL. 它非常冗长,但是看起来服务器正在尝试重定向到/error
,却没有发现它是授权的URL。 Interestingly if I try this in a web browser I get the error formatted with my custom error page (error.html), but if I access it with Postman I just get a JSON message. 有趣的是,如果我在Web浏览器中尝试此操作,则会得到使用自定义错误页面(error.html)格式化的错误,但是如果我使用Postman进行访问,则会收到JSON消息。 A sample of the logs: 日志样本:
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
So it looks like what I maybe need to do is to configure the "authentication failure handler" for the REST API to go to "/error" instead of going to "/login", but only for endpoints under /api/**
. 因此,看来我可能需要做的是将REST API的“身份验证失败处理程序”配置为“ / error”而不是“ / login”,但仅针对/api/**
下的端点。
unsuccessfulAuthentication()
to my Authentication filter 我在我的身份验证过滤器中添加了@successfulAuthentication unsuccessfulAuthentication()
的Override The grandparent class ( AbstractAuthenticationProcessingFilter
) has a method for unsuccessful authentications (ie AuthenticationException
) which delegates to an authentication failure handler class. 祖父母类( AbstractAuthenticationProcessingFilter
)具有用于身份AuthenticationException
失败的方法(即AuthenticationException
),该方法委托给身份验证失败处理程序类。 I could have created my own custom authentication failure handler, but instead decided to simply override the unsuccessfulAuthentication
method with some code that sends back a response with a 401 status and a JSON error message: 我本可以创建自己的自定义身份验证失败处理程序,但决定使用一些代码简单地覆盖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
to make the other errors match ...并添加了自定义AuthenticationEntryPoint
以使其他错误匹配 This doesn't have the exact form of the error messages I had seen at other endpoints, so I also created a custom AuthenticationEntryPoint
(the class that handles unauthorized requests to protected endpoints) and it does basically the same thing. 它没有我在其他端点上看到的错误消息的确切形式,因此我还创建了一个自定义AuthenticationEntryPoint
(处理对受保护端点的未授权请求的类),并且它基本上起到了相同的作用。 My implementation: 我的实现:
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?\"}");
}
}
Now the security configuration for the REST endpoints looks like this (note the addition of " .exceptionHandling().authenticationEntryPoint()
": 现在,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()));
}
}
I'll need to work on my error messages so they're more informative and secure. 我需要处理我的错误消息,以使它们更有用和更安全。 And this doesn't exactly answer my original question, which was how to get those default-style JSON responses I liked, but it does allow me to customize the errors from all types of access failures independently of the web configuration. 这并不能完全回答我最初的问题,即如何获得我喜欢的默认样式的JSON响应,但是它确实允许我独立于所有类型的访问失败而自定义错误,而与Web配置无关。 (Endpoints outside of /api/**
still work with the web form login.) ( /api/**
以外的端点仍可用于Web表单登录。)
Full code as of the current commit: github 截至当前提交的完整代码: github
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.