Javascript请求,用户登陆超时Spring Security如何跳转?
by 伊布
记录一个最近遇到的小问题。
我们有个web应用,使用了React + Spring Boot + Spring Security + cas认证的组合。Spring Security支持cas,代码不赘述,这里讨论用户登陆超时跳转的问题。
用户超时时(默认2小时),点击浏览器的刷新按钮,此时可以重定向到cas的登陆页面。但通常用户未必意识到已经超时了,可能仍然去点击某些控件,由于页面的Js是已经加载ok了的,所以控件可以正常操作,但submit的时候,js向后端发送restful请求,仿佛石沉大海一样,没有任何动静。打开chrome的调试模式,会看到后端返回了302的应答,但chrome因为Js请求不能跨域,拒绝了这个请求。
先来看看cas登录的背景知识。
cas的认证流程
一图胜千言。
cas登录超时与session超时
cas本身有配置超时时间,默认是7200秒。配置在ticketExpirationPolicies.xml
(参考)。
<!-- TicketGrantingTicketExpirationPolicy: Default as of 3.5 -->
<!-- Provides both idle and hard timeouts, for instance 2 hour sliding window with an 8 hour max lifetime -->
<bean id="grantingTicketExpirationPolicy" class="org.jasig.cas.ticket.support.TicketGrantingTicketExpirationPolicy"
p:maxTimeToLiveInSeconds="${tgt.maxTimeToLiveInSeconds:28800}"
p:timeToKillInSeconds="${tgt.timeToKillInSeconds:7200}"/>
timeToKillInSeconds
即登陆超时的时间。
除了cas登录超时,web应用本身也可以控制会话超时,spring boot可以通过如下参数来修改默认的策略。
--server.session.timeout=10 --server.session.cookie.max-age=10
10秒用户无操作,cookie老化,需要重新登录。
开始描述的问题,可以通过spring boot的这两个配置项快速复现出来。
为什么js拿不到302请求?
我们最早的代码,是给spring security直接传了CasAuthenticationEntryPoint
,
http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint()).and()...
@Bean
public AuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(loginUrl);
entryPoint.setServiceProperties(serviceProperties);
}
用户在登录首页时,可以重定向到cas的登录页面;但在10s后,如果有js操作,会在chrome的console里看到如下的报错。
Fetch API cannot load https://192.168.125.66:30443/cas/login?service=http%3A%2F%2F10.84.1.138%3A2222%2Flogin. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://10.84.1.138:2222' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
同时在Network里也会看到js请求的http返回码是302。
在js里加调试信息,会发现,这个302应答,根本没到js,而是被chrome劫走了;而chrome根据302试图跳转时,发现这是一个跨域的请求,而且也没有设置允许跨域,因此报错。
解决办法
到这里,解决办法就比较清晰了:后端针对这种情况不要返回302,而是返回401(未授权);前端拿到401的应答,走logout流程,让用户重新登录。
前端的处理比较简单。
if (response.status == 401) {
window.location.href = "/logout";
return;
}
spring security里实现AuthenticationEntryPoint接口的几个类里,只有CasAuthenticationEntryPoint的commence方法是final的,其他的像BasicAuthenticationEntryPoint的commence方法都可以被override,这样只要自己写个类继承BasicAuthenticationEntryPoint,在commence方法里针对js请求做个特殊处理就行了(类似issue 2999)。
public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint,
InitializingBean {
public final void commence(final HttpServletRequest servletRequest,
final HttpServletResponse response,
final AuthenticationException authenticationException) throws IOException,
ServletException {
final String urlEncodedService = createServiceUrl(servletRequest, response);
final String redirectUrl = createRedirectUrl(urlEncodedService);
preCommence(servletRequest, response);
response.sendRedirect(redirectUrl);
}
如上spring security里CasAuthenticationEntryPoint的源码,由于CasAuthenticationEntryPoint
中没有留出可以在commence中做手脚的地方,在spring security社区问了下这个问题,社区建议favor composition over inheritance,面向对象编程的一个重要思想:组合优先于继承。
所以比较合适的做法是用组合,将这个类组合到自己的类里。
class CasAuthEntryPoint implements AuthenticationEntryPoint {
private CasAuthenticationEntryPoint casAuthenticationEntryPoint;
CasAuthEntryPoint(final String loginUrl, final ServiceProperties serviceProperties) {
casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(loginUrl);
casAuthenticationEntryPoint.setServiceProperties(serviceProperties);
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "UNAUTHORIZED");
} else {
casAuthenticationEntryPoint.commence(request, response, authException);
}
casAuthenticationEntryPoint.commence(request, response, authException);
}
}
最好不要粗暴的把spring security的代码拷贝出来直接改,一个是license问题,再一个是升级问题。
Ref:
Subscribe via RSS