Remember Me


如果想要提供自動登入的功能,可以在登入頁面上提供「記住我」的功能,若使用者決定自動登入,使用 Cookie 來存放自動登入憑據,下次使用者再度造訪時,看看該 Cookie 是否失效,基本上就是自動登入的實作原理。

問題在於,使用簡單的 Cookie 憑據會有容易被偽造的問題,必須對 Cookie 的憑據進行加工使其不易被偽造來避免風險,可以使用 Spring Security 的 Remember Me 功能來簡單達到這項任務。

最簡單的方式,就是在設定頁面防護時,呼叫 rememberMe 方法就可以了:

package cc.openhome.web;

... 略

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { 
    ... 略

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin").hasRole("ADMIN")  
            .antMatchers("/member").hasAnyRole("ADMIN", "MEMBER")
            .antMatchers("/user").authenticated()
            .anyRequest().permitAll()
            .and()
            .rememberMe() // 記住我
            .and()
            .formLogin() 
                .loginPage("/login_page")
                .loginProcessingUrl("/perform_login")
                .failureUrl("/login_page?error")
            .and()
            .logout()
                .logoutUrl("/perform_logout")
                .logoutSuccessUrl("/login_page?logout");
    } 
}

如此用來作為自動登入的 Cookie 憑據預設是兩星期有效期,可以使用 tokenValiditySeconds 自訂有效期,Cookie 的名稱預設是 remember-me,可以使用 rememberMeCookieName 自訂名稱。

登入的頁面中,可以使用 checkbox 類型的輸入欄位,例如:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登入表單</title>
</head>
<body>
    <form method="post" action="perform_login">
        <span th:unless="${param.error == null}" th:text="登入失敗">登入失敗</span>
        <span th:unless="${param.logout == null}" th:text="你已經登出">你已經登出</span>
        <h2>請登入</h2>
        <p>名稱 <input type="text" name="username" required autofocus/></p>
        <p>密碼 <input type="password" name="password" required/></p>
        <p>記住我 <input name="remember-me" type="checkbox"/></p>
        <input type="hidden"  th:name="${_csrf.parameterName}"  th:value="${_csrf.token}" />
        <button type="submit">登入</button>
    </form>
</body>
</html>

預設的請求參數名稱為 remember-me,可以使用 rememberMeParameter 方法來自訂請求參數名稱。如果選擇「記住我」的話,發送的 Cooke 會像是:

Remember Me

Spring Security 會以使用者名稱、密碼、有效期以及一個防止被修改的鍵來編碼 Cooke 值:

BASE64 編碼(名稱 + ":" + 有效期 + MD5 雜湊(名稱 + ":" + 有效期 + ":" + 密碼 + ":" + 鍵))

防止被修改的鍵預設是隨機產生,也可以使用 key 方法來自行設定,在使用者造訪網站時若有這樣的 Cookie 發送過來,Spring Security 可以根據使用者名稱從來取得 Web 應用程式中存放的密碼,經相同運算後,與 Cookie 值比對,若符合就自動登入。

當然,雖然避免了容易被偽造的問題,不過相同有效期間下,產生的 Cookie 值是固定的,還是有 Cookie 被擷取並於有效期內惡意運用的風險。

如果想在每次自動登入後,產生不同的 Cookie 值,可以在每次自動登入時,隨機產生 Token 值加入 Cookie 值的運算,當然,這個隨機產生的 Token,必須存放在 Web 應用程式之中,若想要實作此功能,可以使用 Token Repository。

例如,若想使用資料庫,基於 JDBC,可以建立以下的表格:

CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) NOT NULL,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL,
    PRIMARY KEY (series)
);

Spring Security 提供了 JdbcTokenRepositoryImpl,它實作了 PersistentTokenRepository 介面,實現了 Token Repository 的增刪查找,使用的 SQL 語句,可以在 JdbcTokenRepositoryImpl.java 找到。

基本上,使用 JdbcTokenRepositoryImpl 只需要設定 DataSource 就可以了,為了簡化範例,在這邊使用 H2 的嵌入式資料庫:

package cc.openhome.web;

...略

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { 
    ...略

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin").hasRole("ADMIN")  
            .antMatchers("/member").hasAnyRole("ADMIN", "MEMBER")
            .antMatchers("/user").authenticated()
            .anyRequest().permitAll()
            .and()
            .rememberMe().tokenRepository(persistentTokenRepository())
            .and()
            .formLogin() 
                .loginPage("/login_page")
                .loginProcessingUrl("/perform_login")
                .failureUrl("/login_page?error")
            .and()
            .logout()
                .logoutUrl("/perform_logout")
                .logoutSuccessUrl("/login_page?logout");
    } 

    @Bean(destroyMethod="shutdown")
    public DataSource dataSource(){
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:persistent_logins.sql")
                .build();
    }


    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource());
        return repo;
    }
}

可以看到,在呼叫 rememberMe 之後,可以使用 tokenRepository 方法來設定 PersistentTokenRepository 實例,如此就可以使用資料表格來儲存 Token 值了。

你可以在 RememberMe 找到以上的範例專案。