在使用Spring Security的remember me模块为云端开源论坛XLineCode开发自动登录模块时,本着用户至上的理念优先采用了PersistentTokenBasedRememberMeServices作为实现根基。通过参考Spring Security的官网推荐书籍Spring Security 3 ,很简单也很顺利的在Spring Security命名空间下配置了该service,本地环境也简单测试通过了该功能,但在上线后接近两个月的时间内,却出现了各种多线程引发的问题而导致自动登录功能失效的情况。
Spring Security的remember me模块的两个实现机制简介:
PersistentTokenBasedRememberMeServices:
使用JdbcTokenRepositoryImpl在数据库创建persistent_logins表。当勾选自动登录时随机产生键值对并将该键值对保存在persistent_logins表及浏览器的cookie中,在浏览器当前会话失效后从下一request中提取cookie键值对,通过对比是否与数据库中的键值对一致来判断该自动功能是否有效。有效则根据该key生成新的token并更新至数据库和浏览器。如根据key未能从数据库找到记录,则不进行登录授权并取消该key在浏览器中对应的cookie。如该key对应的cookie token与数据库得到的token不一致,则该cookie在浏览器中可能已经被黑客攻破,被盗用于信息窃取,因此抛出cookie被盗的异常并根据该用户id删除其在数据库对应的所有键值记录(该用户在多个浏览器登录则有多条记录)。
核心方法processAutoLoginCookie代码如下:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { if (cookieTokens.length != 2) { throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); } final String presentedSeries = cookieTokens[0]; final String presentedToken = cookieTokens[1]; PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries); if (token == null) { // No series match, so we can't authenticate using this cookie throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); } // We have a match for this user/series combination if (!presentedToken.equals(token.getTokenValue())) { // Token doesn't match series value. Delete all logins for this user and throw an exception to warn them. tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException(messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); } if (token.getDate().getTime() + getTokenValiditySeconds()*1000L < System.currentTimeMillis()) { throw new RememberMeAuthenticationException("Remember-me login has expired"); } // Token also matches, so login is valid. Update the token value, keeping the *same* series number. if (logger.isDebugEnabled()) { logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'"); } PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date()); try { tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); addCookie(newToken, request, response); } catch (DataAccessException e) { logger.error("Failed to update token: ", e); throw new RememberMeAuthenticationException("Autologin failed due to data access problem"); } return getUserDetailsService().loadUserByUsername(token.getUsername()); }
TokenBasedRememberMeServices:
根据用户id和密码生成cookie,同样通过对比判断cookie是否有效。该机制的缺点是用户修改密码后其在其他浏览器中的cookie不会自动更新,需再次登陆生成cookie。另一缺点是密码经过加密后保存在客户端,存在一定风险。
核心方法processAutoLoginCookie代码如下:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { if (cookieTokens.length != 3) { throw new InvalidCookieException("Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); } long tokenExpiryTime; try { tokenExpiryTime = new Long(cookieTokens[1]).longValue(); } catch (NumberFormatException nfe) { throw new InvalidCookieException("Cookie token[1] did not contain a valid number (contained '" + cookieTokens[1] + "')"); } if (isTokenExpired(tokenExpiryTime)) { throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime) + "'; current time is '" + new Date() + "')"); } // Check the user exists. // Defer lookup until after expiry time checked, to possibly avoid expensive database call. UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]); // Check signature of token matches remaining details. // Must do this after user lookup, as we need the DAO-derived password. // If efficiency was a major issue, just add in a UserCache implementation, // but recall that this method is usually only called once per HttpSession - if the token is valid, // it will cause SecurityContextHolder population, whilst if invalid, will cause the cookie to be cancelled. String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(), userDetails.getPassword()); if (!equals(expectedTokenSignature,cookieTokens[2])) { throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'"); } return userDetails; }
使用PersistentTokenBasedRememberMeServices遇到的各种情况:
1. 因Spring Security的filter链未作并发控制,所以PersistentTokenBasedRememberMeServices的processAutoLoginCookie在处理自动登录时如服务器负载过大用户多次刷新页面会产生线程A和B同时在运行processAutoLoginCookie的情况。假设A是先于B的请求,而A在未执行tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());时被系统挂起了线程,导致B先于A完成了请求。此时A获得资源后执行完时因A请求已被浏览器取消,故A所产生的新的cookie无法在浏览器中更新,导致该浏览器的下次请求中的cookie与数据库中的token不一致从而抛出Cookie被盗的异常(该情况只在极低的概率下才可能发生)。
2. 跑完PersistentTokenBasedRememberMeServices的processAutoLoginCookie后在业务逻辑中如抛出异常而未被代码或者web.xml中的500配置捕获,则会直接返回500错误给浏览器,此时新生成的cookie不会在浏览器更新,同样会导致cookie被盗的情况。不过一般情况下都会捕获,问题不大。
3. filter配置不当导致拦截了页面加载后触发的ajax或一般业务请求:同上,在发生异常时导致cookie更新失败。
4. 在浏览器请求未返回时直接取消该请求,因该请求在服务器执行完后不能正常设置浏览器的cookie,导致浏览器下次请求再次使用失效的cookie时processAutoLoginCookie发现两边token不一致,抛出cookie被盗异常。这种情况在网络不理想的情况下被用户触发的可能性最大,而Spring Security代码中也没有处理该情况的代码。仅仅因为用户取消请求而抛出cookie被盗的异常并清空该用户在其他浏览器的cookie会造成相当差的用户体验,这也是我决定转为采用TokenBasedRememberMeServices的原因.
使用TokenBasedRememberMeServices则相对简单很多,无需考虑并发的情况,只要cookie匹配就可自动登录。
后话:我在发现该情况后分别尝试了oschina,csdn和iteye的登录机制,发现他们均使用的是TokenBasedRememberMeServices的方式。
不错###### 刚看了下OSC的cookie信息如下:
Cookie: oscid=nNj2Yxw98g7t3hxxzDtZ0NiWrJsF%2FkE4v8nIeuKOIJAHMdrnO01Tq6vfb4hXMssHvUMFSLaBVbrw4SYZkmVTbgamWlOOuF2hHVDSC%2BQsw9eqR830wlHgyA%3D%3E
OSC 服务器会验证这个cooike信息,若有效则从这个cooike信息中取出用户id,并且根据用户id从数据库加载用户信息。
是这样吗?
######是,用户id跟密码在cookie中加密了######Spring Security 和 Shiro 哪个更适合和CAS单点登录框架整合?######Shiro还没用过...
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。