我有一个网站,要求在用户操作时异步地在元素内呈现一些HTML.如果用户的会话过期,事情变得棘手,但可以通过创建自定义AuthenticationEntryPoint类来解决,如this SO question和this SO question建议.
我的问题出现了,一旦用户重新登录,因为用户被重定向到最后请求的URL,这恰好是Ajax请求,因此我的用户被重定向到HTML的片段,而不是它浏览的最后一页.
我能够通过删除自定义AuthenticationEntryPoint上的会话属性来解决这个问题:
if (ajaxOrAsync) {
request.getSession().removeAttribute("SPRING_Security_SAVED_REQUEST");
}
这是我的问题的问题.
虽然前面的代码解决了我的问题,但它具有将用户重定向到主页而不是它浏览的最后一页的副作用(因为没有保存的请求).这不会是一个问题,但它会使网站不一致,因为如果最后一个请求是异步请求,它会被重定向到家,但如果是正常请求,它会被重定向到浏览的最后一页. =(
我设法对此进行编码以处理该场景:
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.PortResolver;
import org.springframework.security.web.PortResolverImpl;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import javax.servlet.servletexception;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.commons.lang.StringUtils.isBlank;
public class CustomAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
... // Some not so relevant code
@Override
public void commence(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, servletexception {
... // some code to determine if the request is an ajax request or an async one
if (ajaxOrAsync) {
useRefererAsSavedRequest(request);
response.sendError(SC_UNAUTHORIZED);
} else {
super.commence(request, response, authException);
}
}
private void useRefererAsSavedRequest(final HttpServletRequest request) {
request.getSession().removeAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE);
final URL refererUrl = getRefererUrl(request);
if (refererUrl != null) {
final HttpServletRequestWrapper newRequest = new CustomHttpServletRequest(request, refererUrl);
final PortResolver portResolver = new PortResolverImpl();
final DefaultSavedRequest newSpringSecuritySavedRequest = new DefaultSavedRequest(newRequest, portResolver);
request.getSession().setAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE, newSpringSecuritySavedRequest);
}
}
private URL getRefererUrl(final HttpServletRequest request) {
final String referer = request.getHeader("referer");
if (isBlank(referer)) {
return null;
}
try {
return new URL(referer);
} catch (final MalformedURLException exception) {
return null;
}
}
private class CustomHttpServletRequest extends HttpServletRequestWrapper {
private URL url;
public CustomHttpServletRequest(final HttpServletRequest request, final URL url) {
super(request);
this.url = url;
}
@Override
public String getRequestURI() {
return url.getPath();
}
@Override
public StringBuffer getRequestURL() {
return new StringBuffer(url.toString());
}
@Override
public String getServletPath() {
return url.getPath();
}
}
}
前面的代码解决了我的问题,但这是解决我的重定向问题的一种非常黑客的方法(我克隆并覆盖原始请求……不寒而栗).
所以我的问题是,有没有其他方法可以重写Spring在成功登录后用来重定向用户的链接(考虑到我正在使用的条件)?
我看过Spring的AuthenticationSuccessHandler,但是在Ajax请求失败的情况下,我还没有找到一种方法来传递referer url.
解决方法:
我已经找到了一个可接受的解决方案来解决我的问题,这要归功于reading the docs及之后浏览其他SO answer时的想法.简而言之,我必须创建自己的自定义ExceptionTranslationFilter,并覆盖sendStartAuthentication以不保存请求缓存.
如果看一下ExceptionTranslationFilter代码,它会看起来(对于Finchley SR1):
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws servletexception, IOException {
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response); // <--- Look at me
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
因此,为了不从Ajax请求中保存数据,我应该实现一个CustomExceptionTranslationFilter,其行为如下:
@Override
protected void sendStartAuthentication(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
final AuthenticationException authenticationException) throws servletexception, IOException {
... // some code to determine if the request is an ajax request or an async one
if (isAjaxOrAsyncRequest) {
SecurityContextHolder.getContext().setAuthentication(null);
authenticationEntryPoint.commence(request, response, authenticationException);
} else {
super.sendStartAuthentication(request, response, chain, authenticationException);
}
}
这使得CustomAuthenticationEntryPoint逻辑更加简单:
@Override
public void commence(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, servletexception {
... // some code to determine if the request is an ajax request or an async one, again
if (isAjaxOrAsyncRequest) {
response.sendError(SC_UNAUTHORIZED);
} else {
super.commence(request, response, authException);
}
}
我的CustomWebSecurityConfigurerAdapter应该像这样配置:
@Override
protected void configure(final HttpSecurity http) throws Exception {
final CustomAuthenticationEntryPoint customAuthenticationEntryPoint =
new CustomAuthenticationEntryPoint("/login-path");
final CustomExceptionTranslationFilter customExceptionTranslationFilter =
new CustomExceptionTranslationFilter(customAuthenticationEntryPoint);
http.addFilterafter(customExceptionTranslationFilter, ExceptionTranslationFilter.class)
....
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
....;
}