Spring Security认证成功后回跳(解决前后端分离下O
前言
Spring Security(后面简称SS)用了很长时间了,但之前一直没注意到一个有趣的特性,直到最近弄前后端分离,在OAuth2提供者(github)认证后,需要跳回前端页面(前端页面和服务端不在同个域下),然后突然一般情况下(同域),SS认证后会自动跳回认证前用户想访问的资源。由此开始寻找这个magic。
OAuth2 的一个sso demo,有兴趣可以看一下
问题:SS是怎么在认证成功后自动跳转到认证前用户想访问的资源(url)?
原本我以为SS是跟SS OAuth的实现一样,通过在http报文里面传递这个url,但是看浏览器的报文内容,在认证过程中是没有传递过url的。而且在OAuth里面,OAuth2 provider这种第三方服务,不可能帮你传递这个参数,所以比较好的办法就是利用session,这也解释前后端分离情况下,SS不会(不能)帮我们跳回去前端页面
问题的切口:SavedRequestAwareAuthenticationSuccessHandler
之前我用SS经常接触到这个类,但是我只知道它作为认证成功的handler,在认证成功后会进行一个跳转操作。这里是部分源码
public class SavedRequestAwareAuthenticationSuccessHandler extends
SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
//这里取出了一个request
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL !!关键的一句!!
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
// 这里有一个很明显的跳转操作,追踪targetUrl怎么来的
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
这里有一个很奇怪的属性——savedRequest
,从上面就可以看到,它是来自requestCache
,requestCache
的类型是HttpSessionRequestCache。这里可以看到,第一行代码就从requestCache
的getRequest
方法中取出了savedRequest
,看一下这个方法
public SavedRequest getRequest(HttpServletRequest currentRequest,
HttpServletResponse response) {
HttpSession session = currentRequest.getSession(false);
if (session != null) {
return (SavedRequest) session.getAttribute(SAVED_REQUEST);
}
return null;
}
这里就印证了我们前面的猜测,的确是保存在session里面,那么SS什么时候放进去的?毕竟我想要前后端分离下OAuth2认证后跳回前端页面
很容易猜测是调用HttpSessionRequestCache的setRequest
方法放进去,可以搜索,这里我是用上面的sso demo的日志找到ExceptionTranslationFilter的sendStartAuthentication
方法(把日志级别调到debug)
ExceptionTranslationFilter
ExceptionTranslationFilter
源码里面注释的第一句话就说明了它的用处
Handles any AccessDeniedException and AuthenticationException
thrown within the filter chain.
这里也解释了各种认证filter诸如UsernamePasswordAuthenticationFilter
为什么可以随便抛出AuthenticationException
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
到这里大概明白了SS怎么保存和跳转回认证前用户想访问的资源,总结一下。SS通过ExceptionTranslationFilter
在认证开始前把request缓存到session中,当认证成功后,在SavedRequestAwareAuthenticationSuccessHandler
里取出缓存的request,跳转回认证前用户想访问的url
解决前后端分离下OAuth2认证后跳回前端页面
理解了SS怎么处理认证成功自动跳转问题,解决前后端分离下OAuth2认证成功跳转就很容易了。这里先说一下前后端跨域下OAuth2认证的流程(OAuth2协议具体请自己谷歌,我也说不清楚)
oauth2.png这个流程比同域OAuth2情况下少了一步,同域下第一步应该是访问一个受保护资源,然后才开始上面流程,所以我暂时的做法是
在/login接口处接受一个auth_url参数,表示认证成功后跳转到这个url,然后认证成功后取出这个url进行跳转
这样只需要修改两处地方
继承OAuth2ClientAuthenticationProcessingFilter
class MyOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter{
private RequestCache requestCache = new HttpSessionRequestCache();
public MyOAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
requestCache.saveRequest(request, response);
return super.attemptAuthentication(request, response);
}
}
重写这个filter的AuthenticationSuccessHandler
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
String authUrl = request.getParameter("auth_url");
response.sendRedirect(authUrl);
});
这样就简单粗暴地实现了OAuth2下认证成功后可以自动跳转回认证前想访问的资源了。