今日將與chatgpt協同合作一口氣將前面所用到的寫成一個專案,當中包含登入驗證,撰寫CRUD程式但我們不會串接到資料庫。
{
"success": true,
"message": "XXXX",
"data": [...]
}
驗證訪問/api時的JWT驗證
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
.login-container {
width: 300px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-container h2 {
text-align: center;
margin-bottom: 20px;
}
.login-container input {
width: calc(100% - 20px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.login-container button {
width: 100%;
padding: 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.login-container button:hover {
background-color: #0056b3;
}
.error-message {
color: red;
text-align: center;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<div class="error-message" id="error-message"></div>
<div class="form-group">
<label for="username">使用者帳號</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button onclick="login()">Login</button>
</div>
<script>
function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('error-message');
// 清空錯誤訊息
errorMessage.textContent = '';
// 發送請求至後端 API
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert("login success")
// 跳轉到 crud.html 頁面
window.location.href = '/crud';
//window.open ='/crud';
} else {
// 顯示錯誤訊息
errorMessage.textContent = data.message || 'Login failed!';
}
})
.catch(error => {
// 處理請求錯誤
errorMessage.textContent = 'An error occurred. Please try again.';
console.error('Error:', error);
});
}
</script>
</body>
</html>
crud.html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRUD 操作頁面</title>
<!-- Bootstrap CSS -->
<link
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
rel="stylesheet"
>
<!-- 可選的自定義 CSS -->
<style>
body {
background-color: #f8f9fa;
}
.crud-container {
margin-top: 50px;
}
.crud-table {
background-color: #ffffff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
thead {
background-color: #f2f2f2;
}
tbody {
display: block;
height: 300px; /* 設定 tbody 的高度 */
overflow-y: auto; /* 允許垂直滾動 */
width: 100%;
}
tr {
display: table;
width: 100%;
table-layout: fixed; /* 固定表格布局 */
}
</style>
</head>
<body>
<!-- CRUD 操作頁面內容 -->
<div class="container crud-container">
<div class="row">
<div class="col-md-12">
<div class="crud-table">
<h2 class="mb-4">員工管理</h2>
<button class="btn btn-success mb-3" data-toggle="modal" data-target="#createModal">新增員工</button>
<button id="logout" class="btn btn-danger mb-3" >登出</button>
<table class="table table-bordered table-striped">
<thead class="thead-dark">
<tr>
<th>員工ID</th>
<th>員工名稱</th>
<th>操作</th>
</tr>
</thead>
<tbody id="employeesTableBody">
<!-- 員工數據將由 JavaScript 動態填充 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 新增員工模態框 -->
<div class="modal fade" id="createModal" tabindex="-1" role="dialog" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="createUserForm">
<div class="modal-header">
<h5 class="modal-title" id="createModalLabel">新增員工</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="關閉">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="createUserName">員工名稱</label>
<input type="text" class="form-control" id="createUserName" name="userName" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">新增</button>
</div>
</form>
</div>
</div>
</div>
<!-- 編輯員工模態框 -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="editUserForm">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">編輯員工</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="關閉">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="editUserId" name="userId">
<div class="form-group">
<label for="editUserName">員工名稱</label>
<input type="text" class="form-control" id="editUserName" name="userName" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">存檔</button>
</div>
</form>
</div>
</div>
</div>
<!-- Bootstrap JS 和依賴項 -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
$(document).ready(function() {
// 載入所有員工資料
loadEmployees();
$('#logout').on('click',logout);
// 新增員工表單提交
$('#createUserForm').on('submit', function(event) {
event.preventDefault();
const userName = $('#createUserName').val();
$.ajax({
type: 'POST',
url: '/api/employees',
contentType: 'application/json',
data: JSON.stringify({ name: userName }),
success: function(response) {
alert('新增員工成功!');
$('#createModal').modal('hide');
loadEmployees();
},
error: function(xhr, status, error) {
alert('新增員工失敗:' + xhr.responseText);
}
});
});
// 編輯員工表單提交
$('#editUserForm').on('submit', function(event) {
event.preventDefault();
const userId = $('#editUserId').val();
const userName = $('#editUserName').val();
$.ajax({
type: 'PUT',
url: '/api/employees/' + userId,
contentType: 'application/json',
data: JSON.stringify({ name: userName }),
success: function(response) {
alert('更新員工成功!');
$('#editModal').modal('hide');
loadEmployees();
},
error: function(xhr, status, error) {
alert('更新員工失敗:' + xhr.responseText);
}
});
});
// 編輯按鈕點擊事件
$(document).on('click', '.edit-btn', function() {
const userId = $(this).data('id');
const userName = $(this).data('name');
$('#editUserId').val(userId);
$('#editUserName').val(userName);
$('#editModal').modal('show');
});
// 刪除按鈕點擊事件
$(document).on('click', '.delete-btn', function() {
const userId = $(this).data('id');
if (confirm('確定要刪除這個員工嗎?')) {
$.ajax({
type: 'DELETE',
url: '/api/employees/' + userId,
success: function(response) {
alert('刪除員工成功!');
loadEmployees();
},
error: function(xhr, status, error) {
alert('刪除員工失敗:' + xhr.responseText);
}
});
}
});
// 函數:載入所有員工資料
function loadEmployees() {
$.ajax({
type: 'GET',
url: '/api/employees',
dataType: 'json',
success: function(result) {
console.log("GET /api/employees response:", result); // 增加日誌
if (result.success) {
const tbody = $('#employeesTableBody');
tbody.empty();
// 確保 result.data 是一個陣列
if (Array.isArray(result.data)) {
result.data.forEach(function(employee) {
const row = `
<tr>
<td>${employee.id}</td>
<td>${employee.name}</td>
<td>
<button class="btn btn-primary btn-sm edit-btn" data-id="${employee.id}" data-name="${employee.name}">編輯</button>
<button class="btn btn-danger btn-sm delete-btn" data-id="${employee.id}">刪除</button>
</td>
</tr>
`;
tbody.append(row);
});
} else {
console.error("Expected data to be an array, but got:", typeof result.data);
alert('載入員工資料失敗:資料格式錯誤');
}
} else {
alert('載入員工資料失敗:' + result.message);
}
},
error: function(xhr, status, error) {
console.error("AJAX GET /api/employees error:", error);
alert('載入員工資料失敗:' + xhr.responseText);
}
});
}
function logout() {
// 清除存儲在 cookie 中的 token
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; // 清除 token cookie
alert("logout success!!!");
window.location.href = '/index.html'; // 導向登入頁
}
});
</script>
</body>
</html>
bean - User
public class User {
private String username;
private String password;
//省略getter setter constructor
}
bean - Employee
public class Employee {
private int id;
private String name;
//省略getter setter constructor
}
bean - Result
public class Result<T> {
private boolean success;
private String message;
private T data;
//省略getter setter constructor
}
EmployeeDao
public class EmployeeDao {
private static List<Employee> employees;
static {
employees = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Employee e = new Employee(i,"emp"+i);
employees.add(e);
}
}
public List<Employee> getAllEmployees() {
// 返回員工的複製,以防止外部修改
synchronized (employees) {
return new ArrayList<>(employees);
}
}
public Employee createEmployee(Employee employee) {
// 分配新的ID
int newId = employees.stream().mapToInt(e -> e.getId()).max().getAsInt() +1;
employee.setId(newId);
employees.add(employee);
return employee;
}
public boolean updateEmployee(int id, Employee updatedEmployee) {
synchronized (employees) {
for (Employee emp : employees) {
if (emp.getId() == id) {
emp.setName(updatedEmployee.getName());
return true;
}
}
}
return false; // 未找到該員工
}
public boolean deleteEmployee(int id) {
synchronized (employees) {
return employees.removeIf(emp -> emp.getId() == id);
}
}
}
EmployeeService
public class EmployeeService {
private EmployeeDao employeeDao;
public EmployeeService() {
this.employeeDao = new EmployeeDao();
}
public List<Employee> getAllEmployees() {
return employeeDao.getAllEmployees();
}
public Employee createEmployee(Employee employee) {
return employeeDao.createEmployee(employee);
}
public boolean updateEmployee(int id, Employee employee) {
return employeeDao.updateEmployee(id, employee);
}
public boolean deleteEmployee(int id) {
return employeeDao.deleteEmployee(id);
}
}
LoginServlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private final ObjectMapper objectMapper = new ObjectMapper();
// 假設的員工數據庫驗證(僅為示例,實際應用中請使用安全的驗證方式)
private boolean authenticate(User user) {
// 在這裡進行員工名和密碼的驗證
// 這裡簡單示範,實際應用應該查詢數據庫或其他員工存儲
return "admin".equals(user.getUsername()) && "1234".equals(user.getPassword());
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// 設置回應的內容類型為 JSON 並指定字符編碼
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 解析請求體中的 JSON 數據
User user = JsonUtils.readJson(request, User.class);
System.out.println(user);
// 準備回應的 JSON 數據
Result result = new Result();
if (authenticate(user)) {
// 創建token
String token = JwtUtils.createToken(user.getUsername());
System.out.println(token);
// 設置 Cookie
Cookie jwtCookie = new Cookie("token", token);
jwtCookie.setHttpOnly(true); // 防止 JavaScript 存取,減少 XSS 攻擊風險
jwtCookie.setPath("/");
jwtCookie.setMaxAge(24 * 60 * 60); // 1 天
response.addCookie(jwtCookie);
result.setSuccess(true);
result.setMessage("Login successful!");
} else {
// 如果驗證失敗
result.setSuccess(false);
result.setMessage("Invalid username or password.");
}
JsonUtils.writeJson(response,result);
}
}
CrudServlet
@WebServlet("/crud")
public class CrudServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("WEB-INF/crud.html").forward(req,resp);
}
}
EmployeeServlet
@WebServlet(urlPatterns = {"/api/employees", "/api/employees/*"})
public class EmployeeServlet extends HttpServlet {
private EmployeeService employeeService = new EmployeeService();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Employee> employees = employeeService.getAllEmployees();
Result<List<Employee>> result = new Result<>(true, "Employees fetched successfully", employees);
JsonUtils.writeJson(response, result);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Employee employee = JsonUtils.readJson(request, Employee.class);
employeeService.createEmployee(employee);
Result result = new Result(true, "Employee saved successfully");
JsonUtils.writeJson(response, result);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Employee employee = JsonUtils.readJson(req, Employee.class);
String pathInfo = req.getPathInfo();
String empId = pathInfo.split("/")[1];
employee.setId(Integer.parseInt(empId));
boolean isSuccess = employeeService.updateEmployee(employee.getId(), employee);
String message = isSuccess?"Employee updated successfully":"Employee update failed";
Result result = new Result(isSuccess, message);
JsonUtils.writeJson(resp, result);
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String pathInfo = req.getPathInfo();
String empId = pathInfo.split("/")[1];
boolean isSuccess = employeeService.deleteEmployee(Integer.parseInt(empId));
String message = isSuccess?"Employee deleted successfully":"Employee deleted failed";
Result result = new Result(isSuccess, message);
JsonUtils.writeJson(resp, result);
}
}
JwtAuthenticationFilter
@WebFilter("/api/*")
public class JwtAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest)request;
System.out.println(req.getRequestURI());
System.out.println("filter do.......");
// 從 Cookie 中獲取 JWT Token
String token = getTokenFromCookies((HttpServletRequest) request);
System.out.println("token: " + token);
if (token != null && JwtUtils.validateToken(token)) {
System.out.println("filter passed");
chain.doFilter(request, response);
}else {
System.out.println("filter redirect to index.html");
res.sendRedirect("/index.html");
}
}
// 從 Cookie 中獲取 JWT Token
private String getTokenFromCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null); // 返回找到的 JWT Token,或 null
}
return null;
}
}
JsonUtils
public class JsonUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
public static <T> T readJson(HttpServletRequest rq ,Class<T> clazz) throws IOException {
// 解析請求體中的 JSON 數據
BufferedReader reader = rq.getReader();
T t = objectMapper.readValue(reader.readLine(), clazz);
return t;
}
public static void writeJson(HttpServletResponse response, Result result){
response.setContentType("application/json;charset=UTF-8");
try {
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(json);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
JwtUtils
public class JwtUtils {
private static final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Token 有效期設置
private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 12; // 60 * 12 minutes
public static String createToken(String username) {
String token = Jwts.builder()
.setSubject(username) // 使用者名稱
.setIssuedAt(new Date()) // Token 發行時間
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key) // 使用自定義 Secret Key 簽名
.compact();
return token;
}
public static boolean validateToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key) // 使用相同的 Secret Key 來驗證 Token
.build()
.parseSignedClaims(token)
.getBody();
boolean isValid = claims.getExpiration().after(new Date());
System.out.println(isValid);
System.out.println(claims);
return true;
}catch(ExpiredJwtException e){
System.out.println("token expired");
e.printStackTrace();
return false;
}catch(SignatureException e){
System.out.println("signature error");
e.printStackTrace();
return false;
}
}
}