feat: protect learning pages and rebuild cockpit

This commit is contained in:
Codex
2026-03-24 17:07:40 +08:00
parent 368a5061b0
commit d81750aaf9
8 changed files with 430 additions and 809 deletions

View File

@@ -3,8 +3,12 @@ package com.example.demo.controller.auth;
import com.example.demo.common.ApiResponse;
import com.example.demo.dto.auth.LoginRequest;
import com.example.demo.security.LearningJwtUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -13,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.Map;
@RestController
@@ -26,18 +31,28 @@ public class LearningAuthController {
}
@PostMapping("/login")
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest request) {
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest request, HttpServletResponse response) {
if (!(("admin".equals(request.username()) && "admin123".equals(request.password()))
|| ("user".equals(request.username()) && "user123".equals(request.password())))) {
return new ApiResponse<>(401, "Invalid demo credentials", null, java.time.Instant.now());
}
String token = jwtUtil.generateToken(request.username());
response.addHeader(HttpHeaders.SET_COOKIE, authCookie(token).toString());
return ApiResponse.ok(Map.of(
"token", token,
"type", "Bearer",
"username", request.username(),
"tip", "Attach Authorization: Bearer <token> when calling protected lab APIs."
"tip", "Attach Authorization: Bearer <token> when calling protected lab APIs or rely on the demo auth cookie for page access."
));
}
@PostMapping("/logout")
public ApiResponse<Map<String, Object>> logout(HttpServletResponse response) {
response.addHeader(HttpHeaders.SET_COOKIE, clearAuthCookie().toString());
return ApiResponse.ok(Map.of(
"cleared", true,
"tip", "Frontend local storage should be cleared too."
));
}
@@ -45,21 +60,22 @@ public class LearningAuthController {
public ApiResponse<Map<String, Object>> mode() {
return ApiResponse.ok(Map.of(
"mode", "learning-jwt",
"protectedPaths", new String[]{"/api/users/**", "/aop/**", "/api/lab/**", "/learn/**", "/api/secure/**"},
"protectedPaths", new String[]{"/index.html", "/users.html", "/aop.html", "/events.html", "/api/users/**", "/aop/**", "/api/lab/**", "/learn/**", "/api/secure/**"},
"defaultAccounts", new String[]{"admin/admin123", "user/user123"},
"note", "Use this demo login before opening the advanced labs on a public VPS."
"note", "Login now returns both a bearer token and an auth cookie so protected pages and APIs can be studied together."
));
}
@GetMapping("/introspect")
public ApiResponse<Map<String, Object>> introspect(
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization,
HttpServletRequest request
) {
if (!StringUtils.hasText(authorization) || !authorization.startsWith("Bearer ")) {
String token = resolveToken(authorization, request);
if (!StringUtils.hasText(token)) {
return ApiResponse.fail(401, "Missing bearer token");
}
String token = authorization.substring(7);
if (!jwtUtil.validate(token)) {
return ApiResponse.fail(401, "Token is invalid or expired");
}
@@ -70,4 +86,40 @@ public class LearningAuthController {
"tip", "JWT is self-contained: the server can read claims without storing a session row for each request."
));
}
private String resolveToken(String authorization, HttpServletRequest request) {
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "";
}
for (Cookie cookie : cookies) {
if (LearningJwtUtil.AUTH_COOKIE_NAME.equals(cookie.getName()) && StringUtils.hasText(cookie.getValue())) {
return cookie.getValue();
}
}
return "";
}
private ResponseCookie authCookie(String token) {
return ResponseCookie.from(LearningJwtUtil.AUTH_COOKIE_NAME, token)
.httpOnly(true)
.sameSite("Lax")
.path("/")
.maxAge(Duration.ofMillis(jwtUtil.expirationMillis()))
.build();
}
private ResponseCookie clearAuthCookie() {
return ResponseCookie.from(LearningJwtUtil.AUTH_COOKIE_NAME, "")
.httpOnly(true)
.sameSite("Lax")
.path("/")
.maxAge(Duration.ZERO)
.build();
}
}