今日目標,在頁面檢驗登入、使用 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>
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";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<div>
<p th:text="${name}"></p>
</div>
</body>
</html>
.anyRequest().authenticated()
改為 .anyRequest().permitAll()
)<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<div>
<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>
</div>
</body>
</html>
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
,這個是讓 IntelliJ 辨識語法 sec:
用的,與前面的 th 相同sec:authorize
:設定權限,當有某個權限時,這個欄位才會出現
isAuthenticated()
,因此上方的 register、login 連結我們是設定沒有登入才顯示,而 logout 的連結則是有登入才會顯示<sec:authorize access="hasRole('ADMIN')"></sec:authorize>
我們只有在首頁才出現 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>
<!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"
>
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<nav>
<ul>
<li>
<a href="/login" sec:authorize="!isAuthenticated()">Login</a>
</li>
<li class="nav-item">
<a href="/register" sec:authorize="!isAuthenticated()">Register</a>
</li>
<li class="nav-item">
<a href="/logout" sec:authorize="isAuthenticated()">Logout</a>
</li>
</ul>
</nav>
<div layout:fragment="content">
<p>This is filled by the content template.</p>
</div>
</body>
</html>
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>
填充<!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}"
>
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<div layout:fragment="content">
<h1>Home Page</h1>
<p th:text="${name}"></p>
</div>
</body>
</html>
layout:decorate="~{layout.html}"
,這表示我們使用的模板是 layout.html<div layout:fragment="content">...</div>
就是填充原先在 layout.html 留下的區塊,區塊的名稱為 content,在這塊底下撰寫的內容,會注入到原先模板擁有相同名稱的區塊layout:fragment="content"
所定義的
小弟我提供一個使用 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"
>
<head>
<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>
<body>
<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>
</li>
</ul>
<ul class="navbar-nav d-flex">
<li class="nav-item">
<a href="/login" class="nav-link" sec:authorize="!isAuthenticated()">Login</a>
</li>
<li class="nav-item">
<a href="/register" class="nav-link" sec:authorize="!isAuthenticated()">Register</a>
</li>
<li class="nav-item">
<a href="/logout" class="nav-link" sec:authorize="isAuthenticated()">Logout</a>
</li>
</ul>
</div>
</nav>
<div layout:fragment="content">
<p>This is filled by the content template.</p>
</div>
<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}"
>
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<div layout:fragment="content">
<h1>Home Page</h1>
<p th:text="${name}"></p>
</div>
</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}"
>
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<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>
</div>
</div>
<br>
<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>
<div class="form-group">
<label for="username">帳號</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username" th:field="*{username}">
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" th:field="*{password}">
</div>
<div th:if="${error}" class="alert alert-danger">
<div th:text="${error}"></div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">註冊</button>
</div>
<br>
<small class="form-text text-center">
已經有帳號,<a href="/login">登入</a>
</small>
</form>
</div>
</div>
</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}"
>
<head>
<meta charset="UTF-8">
<title>Cards</title>
</head>
<body>
<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>
</div>
</div>
<br>
<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>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<div th:if="${param.error}" class="alert alert-danger">
<div>帳號或密碼錯誤</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">登入</button>
</div>
<br>
<small class="form-text text-center">
還沒註冊嗎,<a href="/register">註冊</a>
</small>
</form>
</div>
</div>
</body>
</html>