1. 程式人生 > >Spring Security之Remember me詳解

Spring Security之Remember me詳解

boot 請求 .get override ret width attack size private

Remember me功能就是勾選"記住我"後,一次登錄,後面在有效期內免登錄。

先看具體配置:

pom文件:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Security的配置:

    @Autowired
    private UserDetailsService myUserDetailServiceImpl; // 用戶信息服務

    @Autowired
    private DataSource dataSource; // 數據源

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // formLogin()是默認的登錄表單頁,如果不配置 loginPage(url),則使用 spring security
        
// 默認的登錄頁,如果配置了 loginPage()則使用自定義的登錄頁 http.formLogin() // 表單登錄 .loginPage(SecurityConst.AUTH_REQUIRE) .loginProcessingUrl(SecurityConst.AUTH_FORM) // 登錄請求攔截的url,也就是form表單提交時指定的action .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) .and() .rememberMe()
.userDetailsService(myUserDetailServiceImpl)
// 設置userDetailsService .tokenRepository(persistentTokenRepository()) // 設置數據訪問層 .tokenValiditySeconds(60 * 60) // 記住我的時間(秒) .and() .authorizeRequests() // 對請求授權 .antMatchers(SecurityConst.AUTH_REQUIRE, securityProperty.getBrowser().getLoginPage()).permitAll() // 允許所有人訪問login.html和自定義的登錄頁 .anyRequest() // 任何請求 .authenticated()// 需要身份認證 .and() .csrf().disable() // 關閉跨站偽造 ; } /** * 持久化token * * Security中,默認是使用PersistentTokenRepository的子類InMemoryTokenRepositoryImpl,將token放在內存中 * 如果使用JdbcTokenRepositoryImpl,會創建表persistent_logins,將token持久化到數據庫 */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 設置數據源 // tokenRepository.setCreateTableOnStartup(true); // 啟動創建表,創建成功後註釋掉 return tokenRepository; }

上面的myUserDetailServiceImpl是自己實現的UserDetailsService接口,dataSource會自動讀取數據庫配置。過期時間設置的3600秒,即一個小時

在登錄頁面加一行(name必須是remeber-me):

技術分享圖片

"記住我"基本原理:

技術分享圖片

1、第一次發送認證請求,會被UsernamePasswordAuthenticationFilter攔截,然後身份認證。認證成功後,在AbstracAuthenticationProcessingFilter中,有個RememberMeServices接口。該接口默認實現類是NullRememberMeServices,這裏會調用另一個實現抽象類AbstractRememberMeServices

    // ...

    private RememberMeServices rememberMeServices = new NullRememberMeServices();

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {

        // ...

        SecurityContextHolder.getContext().setAuthentication(authResult);

        // 登錄成功後,調用RememberMeServices保存Token相關信息
        rememberMeServices.loginSuccess(request, response, authResult);

        // ...
    }

2、調用AbstractRememberMeServices的loginSuccess方法。可以看到如果request中name為"remember-me"為true時,才會調用下面的onLoginSuccess()方法。這也是為什麽上面登錄頁中的表單,name必須是"remember-me"的原因:

技術分享圖片

3、在Security中配置了rememberMe()之後, 會由PersistentTokenBasedRememberMeServices去實現父類AbstractRememberMeServices中的抽象方法。PersistentTokenBasedRememberMeServices中,有一個PersistentTokenRepository,會生成一個Token,並將這個Token寫到cookie裏面返回瀏覽器。PersistentTokenRepository的默認實現類是InMemoryTokenRepositoryImpl,該默認實現類會將token保存到內存中。這裏我們配置了它的另一個實現類JdbcTokenRepositoryImpl,該類會將Token持久化到數據庫中

    // ...

    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();

    protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();

        logger.debug("Creating new persistent login for user " + username);

        // 創建一個PersistentRememberMeToken
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            // 保存Token
            tokenRepository.createNewToken(persistentToken);
            // 將Token寫到Cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

4、JdbcTokenRepositoryImpl將Token持久化到數據庫

   /** The default SQL used by <tt>createNewToken</tt> */
    public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";

    public void createNewToken(PersistentRememberMeToken token) {
        getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
                token.getTokenValue(), token.getDate());
    }

查看數據庫,可以看到往persistent_logins 中插入了一條數據:

技術分享圖片

5、重啟服務,發送第二次認證請求,只會攜帶Cookie。所以直接會被RememberMeAuthenticationFilter攔截,並且此時內存中沒有認證信息。可以看到,此時的RememberMeServices是由PersistentTokenBasedRememberMeServices實現

技術分享圖片

6、在PersistentTokenBasedRememberMeServices中,調用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) + "");
        }

        // 從Cookie中獲取Series和Token
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1]; 

        //在數據庫中,通過Series查詢PersistentRememberMeToken
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        if (token == null) {
            throw new RememberMeAuthenticationException(
                    "No persistent token found for series id: " + presentedSeries);
        }

        // 校驗數據庫中Token和Cookie中的Token是否相同
        if (!presentedToken.equals(token.getTokenValue())) {
            tokenRepository.removeUserTokens(token.getUsername());

            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }

        // 判斷Token是否超時
        if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
                .currentTimeMillis()) {
            throw new RememberMeAuthenticationException("Remember-me login has expired");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Refreshing persistent login token for user ‘"
                    + token.getUsername() + "‘, series ‘" + token.getSeries() + "");
        }
        
        // 創建一個新的PersistentRememberMeToken
        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());

        try {
            //更新數據庫中Token
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            //重新寫到Cookie
            addCookie(newToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to update token: ", e);
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
        //調用UserDetailsService獲取用戶信息
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

7、獲取用戶相關信息後,再調用AuthenticationManager去認證授權,授權細節可參考:AuthenticationManager、ProviderManager

Spring Security之Remember me詳解