今日目標,在頁面檢驗登入、使用 Thymeleaf Page Layout 作為模板,建構網頁。

在前端檢驗登入狀態

雖然昨天我們在後端的部分檢驗登入狀態,避免使用者已經登入但還能進入 register、login 頁面,但在前端顯示的時候,我們希望未登入可以顯示 register、login 的按鈕,當登入後顯示 logout 的按鈕,這時候就可以使用 spring security tag。

  • 添加依賴項:
    <!-- sec -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
    
  • 在這之前,我們先把之前的 HelloWorld 移除,建立一個 package 名稱為 home,然後在底下建立 HomeController,內容為:
    package com.example.home;
    import com.example.user.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    @Controller
    public class HomeController {
        @Autowired
        private UserService userService;
        @GetMapping({"/", "/home"})
        public String viewHomePage(Model model) {
            String name = userService.getUsername();
            model.addAttribute("name", name);
            return "home";
    
  • 在 templates 底下建立 html file,名稱為 home,內容為:
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
        <p th:text="${name}"></p>
    </body>
    </html>
    
  • 到這邊,我們只是先建立 home page,並且在登入後顯示自己的使用者帳號(為了之後測試方便,可以在 WebSecurityConfig 裡將 .anyRequest().authenticated() 改為 .anyRequest().permitAll())
  • 再來,我們要在首頁加入 register、login、logout 的連結,修改 home.html 的內容:
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
        <p th:text="${name}"></p>
        <a href="/login" sec:authorize="!isAuthenticated()">Login</a>
        <a href="/register" sec:authorize="!isAuthenticated()">Register</a>
        <a href="/logout" sec:authorize="isAuthenticated()">Logout</a>
    </body>
    </html>
    
  • 注意第二行的 xmlns:sec="http://www.thymeleaf.org/extras/spring-security",這個是讓 IntelliJ 辨識語法 sec: 用的,與前面的 th 相同
  • sec:authorize:設定權限,當有某個權限時,這個欄位才會出現
  • 我們單純看他有沒有登入,所以權限的設定用 isAuthenticated(),因此上方的 register、login 連結我們是設定沒有登入才顯示,而 logout 的連結則是有登入才會顯示
  • 亦可直接當作 html tag 使用,並配合 access 根據不同的角色做權限管理,例子: <sec:authorize access="hasRole('ADMIN')"></sec:authorize>
  • 更多例子可參考下方的 參考資料~~
  • Thymeleaf Page Layout

    我們只有在首頁才出現 register、login、logout 的連結不太合理,通常應該有個導覽列(navigation bar, nav bar)負責放這些資訊,但每個頁面都會有這個導覽列,我們不可能每個頁面都重複的複製貼上,如果之後想改樣式或內容就必須全部頁面都改,這個方法顯然欠缺可維護姓,這時候就要使用「模板」的概念。
    如果讀者曾經接觸過前端框架,應該就有模板的概念,所謂模板就是可以只寫一個頁面架構,然後讓其他的頁面都套用同樣的架構,只針對頁面的主要內容(content)作個別設計,舉例來說,我們目前每個頁面都需要一個統一的導覽列,那我們就可以寫一個模板設計好導覽列後,讓其他頁面套用,其他頁面只需要針對網頁內容設計,而不需要再次對導覽列作設計。
    我們在前面使用了 Thymeleaf,他也同樣有提供模板 Thymeleaf Page Layout,小弟接下來將先以此實作模板並介紹基本用法和範例,並在這之後使用 bootstrap 來美化整個頁面。

  • 添加依賴項:
    <!-- thymeleaf layout -->
    <dependency>
        <groupId>nz.net.ultraq.thymeleaf</groupId>
        <artifactId>thymeleaf-layout-dialect</artifactId>
    </dependency>
    
  • 在 templates 底下建立一個 html file,名稱為 layout,內容為:
    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
                <a href="/login" sec:authorize="!isAuthenticated()">Login</a>
            <li class="nav-item">
                <a href="/register" sec:authorize="!isAuthenticated()">Register</a>
            <li class="nav-item">
                <a href="/logout" sec:authorize="isAuthenticated()">Logout</a>
    <div layout:fragment="content">
        <p>This is filled by the content template.</p>
    </body>
    </html>
    
  • 注意第 5 行,xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout",同樣是為了讓 IntelliJ 辨識特殊語法,layout:fragment layout:fragment="content":定義一個區塊名稱為 content,這是讓後續套用這個模板的頁面填充這個區塊來作為網頁的內容,如果沒有填充這個區塊,就會以這邊預設的內容填充,此處例子是以 <p>This is filled by the content template.</p> 填充
  • 改寫 home.html 的頁面
    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout.html}"
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
    <div layout:fragment="content">
        <h1>Home Page</h1>
        <p th:text="${name}"></p>
    </body>
    </html>
    
  • 注意到第 6 行 layout:decorate="~{layout.html}",這表示我們使用的模板是 layout.html
  • 下方的 <div layout:fragment="content">...</div> 就是填充原先在 layout.html 留下的區塊,區塊的名稱為 content,在這塊底下撰寫的內容,會注入到原先模板擁有相同名稱的區塊
  • 回到 http://127.0.0.1:8080/ 頁面,應該會看到同樣的畫面,上方的連結是原先在模板(layout.html)定義的,而下方的內容則是在 home.html 的 layout:fragment="content" 所定義的

    使用 Bootstrap 美化頁面

    小弟我提供一個使用 bootstrap 設計的簡單頁面,如果讀者有更好的想法就自己設計吧~~ 如果只是想學習 spring boot 以及 thymeleaf 的讀者也可以直接跳過這部分,這並不影響後續的內容實作 (只是有介面之後 demo 比較舒服啦)

    在 static 底下建立 css 資料夾,再建立 main.css,內容為:

    .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
        margin-bottom: 0 !important;
    .game-window {
        height: 630px;
        width: 1200px;
        margin: 20px auto 0 auto;
        border-width: 10px;
    .page-title {
        display: inline-block;
        padding: 10px 20px;
        margin-top: 10px;
        z-index: 9;
    .login-panel, .register-panel {
        margin: 30px auto;
        padding: 30px 150px 30px 150px;
    

    在 static 底下建立 js 資料夾,再建立 main.js,內容為:

    var jq = $.noConflict();
    

    layout.html

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        <meta charset="UTF-8">
        <title>Cards</title>
        <!-- bootstrap -->
        <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
                integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
                crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"
                integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct"
                crossorigin="anonymous"></script>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
            integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N"
            crossorigin="anonymous">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
        <!-- jquery -->
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"
                integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
                crossorigin="anonymous"></script>
        <!-- custom -->
        <script type="text/javascript" th:src="@{/js/main.js}"></script>
        <link th:href="@{/css/main.css}" rel="stylesheet">
    </head>
        <nav class="navbar navbar-expand-lg navbar-light" style="background-color: #e3f2fd;">
            <a class="navbar-brand" href="/">Cards</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a href="/rooms" class="nav-link">All Rooms</a>
                <ul class="navbar-nav d-flex">
                    <li class="nav-item">
                        <a href="/login" class="nav-link" sec:authorize="!isAuthenticated()">Login</a>
                    <li class="nav-item">
                        <a href="/register" class="nav-link" sec:authorize="!isAuthenticated()">Register</a>
                    <li class="nav-item">
                        <a href="/logout" class="nav-link" sec:authorize="isAuthenticated()">Logout</a>
        <div layout:fragment="content">
            <p>This is filled by the content template.</p>
        <div layout:fragment="js-and-css"></div>
    </body>
    </html>
    

    home.html

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout.html}"
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
    <div layout:fragment="content">
        <h1>Home Page</h1>
        <p th:text="${name}"></p>
    </body>
    </html>
    

    register.html

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout.html}"
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
    <div layout:fragment="content" class="card game-window">
        <div class="card register-panel">
            <div class="text-center">
                <div class="card page-title">
                    <div class="h3">註冊</div>
            <form action="/register" method="post" th:object="${user}">
                <div class="form-group">
                    <label for="email">信箱</label>
                    <input type="email" class="form-control" id="email" name="email" placeholder="Email" th:field="*{email}">
                <div class="form-group">
                    <label for="username">帳號</label>
                    <input type="text" class="form-control" id="username" name="username" placeholder="Username" th:field="*{username}">
                <div class="form-group">
                    <label for="password">密碼</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="Password" th:field="*{password}">
                <div th:if="${error}" class="alert alert-danger">
                    <div th:text="${error}"></div>
                <div class="text-center">
                    <button type="submit" class="btn btn-primary">註冊</button>
                <small class="form-text text-center">
                    已經有帳號,<a href="/login">登入</a>
                </small>
            </form>
    </body>
    </html>
    

    login.html

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout.html}"
        <meta charset="UTF-8">
        <title>Cards</title>
    </head>
    <div layout:fragment="content" class="card game-window">
        <div class="card login-panel">
            <div class="text-center">
                <div class="card page-title">
                    <div class="h3">登入</div>
            <form action="/login" method="post">
                <div class="form-group">
                    <label for="username">帳號</label>
                    <input type="text" class="form-control" id="username" name="username" placeholder="Username">
                <div class="form-group">
                    <label for="password">密碼</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="Password">
                <div th:if="${param.error}" class="alert alert-danger">
                    <div>帳號或密碼錯誤</div>
                <div class="text-center">
                    <button type="submit" class="btn btn-primary">登入</button>
                <small class="form-text text-center">
                    還沒註冊嗎,<a href="/register">註冊</a>
                </small>
            </form>
    </body>
    </html>
    30. JSP Tag Libraries
    Spring Security JSP Tag Library
    Thymeleaf Page Layouts
    
  •