自訂登入登出頁面


在〈使用 Spring Security〉中,使用的是自動產生的登入與登出頁面,它們是由 DefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilter 產生,可以在 org/springframework/security/web/authentication/ui 找到原始碼。

自動產生的頁面若不符合 Web 應用程式風格,下一步自然就會想要自訂這些頁面,首先,必須在 SecurityConfig 中設定相關資訊:

package cc.openhome.web;

... 略

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login_page", "/logout_page", "/perform_login", "/perform_logout").permitAll()
            .antMatchers("/**").authenticated()
            .and()
            .formLogin() 
                .loginPage("/login_page")
                .loginProcessingUrl("/perform_login")
                .failureUrl("/login_page?error")
            .and()
            .logout()
                .logoutUrl("/perform_logout")
                .logoutSuccessUrl("/login_page?logout");
    }
}

在這邊重新定義了 configure(HttpSecurity http) 方法,因為登入、登出頁面,以及實際接受登入、登出請求的 URI 必須可以直接請求,在這邊使用 antMatchers("/login_page", "/logout_page", "/perform_login", "/perform_logout") 設定符合的頁面清單,antMatchers 表示使用 Ant 比對表示式,必須的話,也可以使用 regexMatchers 以 Regular expression 來設定比對清單,比對清單設定好之後,使用 permitAll 表示,這些清單允許各種請求;接下來 antMatchers("/**").authenticated() 則表示,其他全部的路徑,都得經過使用者驗證後才可以存取。

接著表單登入的部份,於呼叫 formLogin 方法後進行設定,loginPage 設定顯示自訂表單的路徑,loginProcessingUrl 設定允許 POST 登入資訊的路徑(也就是表單 action 屬性的對象),登入失敗要重新導向的路徑與請求參數,則是使用 failureUrl 來設定。

登出設定的話,必須呼叫 logout 方法,logoutUrl 是接受 POST 登出請求的對象,logoutSuccessUrl 則是登出後要重新導向的路徑。

在這邊模仿了〈使用 Spring Security〉中,登入錯誤會附上 error 請求參數,而登出成功會附上 logout,相關的頁面可以藉此來決定是否顯示對應的登入錯誤或登出成功資訊,為了要能有這個動態性,可以建立一個控制器,用來顯示登入與登出頁面:

package cc.openhome.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginOutPageController {
    @GetMapping("login_page")
    public String login_page() {
        return "login";
    }

    @GetMapping("logout_page")
    public String logout_page() {
        return "logout";
    }
}

其實像上頭這種直接對應至畫面的處理器,也可以在 WebConfig 中這麼設定:

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/login_page").setViewName("login");
    registry.addViewController("/logout_page").setViewName("logout");
}    

不過,因為這個設定會在沒有符合的處理器下才會套用,而目前的專案範例中,FooController 有個 @GetMapping("/{path}") 處理器,對於請求 login_pagelogout_page,會因符合 @GetMapping("/{path}") 而套用該處理器,而不是套用 WebConfig 的設定,因此範例專案中還是使用 LoginOutPageController

因為專案使用 Thymeleaft 模版,根據專案中的設定,登入頁面的要在 WEB-INF/templates 中新增 login.html:

<!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>
        <input type="hidden"  th:name="${_csrf.parameterName}"  th:value="${_csrf.token}" />
        <button type="submit">登入</button>
    </form>
</body>
</html> 

在上頭可以看到判斷是否有 errorlogout 請求參數,以顯示對應訊息的 span;根據方才 SecurityConfig 的設定,登入表單發送的 action 設為 "perform_login"

另外,為了防範 CSRF,Spring Security 預設啟用 CSRF Token,也就是對表單請求額外產生一個 Token 作為憑據,通常作為隱藏欄位安插在表單之中,在 Thymeleaf 或 JSP 中,Token 名稱與值可分別使用 ${_csrf.parameterName}${_csrf.token} 來取得,發送請求時,必須得包含這個 Token,否則就會被拒絕請求。

你可以觀察〈使用 Spring Security〉中預設的登入頁面 HTML,其中就有 CSRF Token,它長得像這樣:

<input name="_csrf" type="hidden" value="729b3938-7aed-4e42-a118-09c63dc22372" />

如果不想要啟用 CSRF 防護,可以呼叫 disable().csrf() 來停用。

你可以在必要的頁面中提供一個登出鏈結,例如在目前專案的 foo.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Foo</title>
</head>
<body>
    You're requesting <b><span th:text="${path}">path</span></b>.
    <p><a href="logout_page">登出</a></p>
</body>
</html>

按下鏈結後會顯示登出頁面,根據目前專案的設定,這要撰寫在 WEB-INF/templates 的 logout.html:

<!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_logout">
        <input type="hidden"  th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        <button type="submit">確定要登出嗎?</button>
    </form>
</body>
</html>

接著你可以重新部署應用程式,看看自訂頁面是否成功,你可以在 LoginOutForm 找到以上的範例專案。