feat: finish bilingual learning auth cockpit
This commit is contained in:
@@ -16,7 +16,7 @@ public record ApiResponse<T>(
|
||||
return new ApiResponse<>(0, message, data, Instant.now());
|
||||
}
|
||||
|
||||
public static ApiResponse<Void> fail(int code, String message) {
|
||||
public static <T> ApiResponse<T> fail(int code, String message) {
|
||||
return new ApiResponse<>(code, message, null, Instant.now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.common.ApiResponse;
|
||||
import com.example.demo.security.LearningJwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/lab")
|
||||
public class AdvancedLabController {
|
||||
|
||||
private final RequestMappingHandlerMapping handlerMapping;
|
||||
private final LearningJwtUtil jwtUtil;
|
||||
|
||||
public AdvancedLabController(
|
||||
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping,
|
||||
LearningJwtUtil jwtUtil
|
||||
) {
|
||||
this.handlerMapping = handlerMapping;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@GetMapping("/reflection/routes")
|
||||
public ApiResponse<Map<String, Object>> reflectionRoutes() {
|
||||
List<Map<String, Object>> routes = handlerMapping.getHandlerMethods().entrySet().stream()
|
||||
.map(this::toRouteView)
|
||||
.sorted((left, right) -> left.get("path").toString().compareTo(right.get("path").toString()))
|
||||
.toList();
|
||||
|
||||
List<Map<String, Object>> classSummaries = List.of(
|
||||
summarizeClass(UserController.class),
|
||||
summarizeClass(LearnController.class),
|
||||
summarizeClass(AopEventController.class),
|
||||
summarizeClass(com.example.demo.controller.auth.LearningAuthController.class)
|
||||
);
|
||||
|
||||
return ApiResponse.ok(Map.of(
|
||||
"routeCount", routes.size(),
|
||||
"routes", routes,
|
||||
"classSummaries", classSummaries,
|
||||
"tip", "Use reflection to connect annotations, method signatures, and runtime route registration."
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/reflection/user-model")
|
||||
public ApiResponse<Map<String, Object>> reflectUserModel() {
|
||||
List<Map<String, Object>> fields = Arrays.stream(com.example.demo.model.User.class.getDeclaredFields())
|
||||
.map(this::toFieldView)
|
||||
.toList();
|
||||
|
||||
return ApiResponse.ok(Map.of(
|
||||
"className", com.example.demo.model.User.class.getName(),
|
||||
"fieldCount", fields.size(),
|
||||
"fields", fields,
|
||||
"tip", "Reflection makes frameworks possible because metadata can be discovered at runtime."
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/concurrency/simulate")
|
||||
public ApiResponse<Map<String, Object>> simulateConcurrency(
|
||||
@RequestParam(defaultValue = "12") int tasks,
|
||||
@RequestParam(defaultValue = "4") int poolSize
|
||||
) {
|
||||
int safeTasks = Math.max(1, Math.min(tasks, 32));
|
||||
int safePoolSize = Math.max(1, Math.min(poolSize, 8));
|
||||
ExecutorService executor = Executors.newFixedThreadPool(safePoolSize);
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
Instant start = Instant.now();
|
||||
|
||||
try {
|
||||
List<CompletableFuture<Map<String, Object>>> jobs = createJobs(safeTasks, counter, executor);
|
||||
CompletableFuture.allOf(jobs.toArray(CompletableFuture[]::new)).join();
|
||||
List<Map<String, Object>> results = jobs.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.toList();
|
||||
|
||||
return ApiResponse.ok(Map.of(
|
||||
"tasks", safeTasks,
|
||||
"poolSize", safePoolSize,
|
||||
"finalCounter", counter.get(),
|
||||
"durationMs", Duration.between(start, Instant.now()).toMillis(),
|
||||
"sample", results.stream().limit(5).toList(),
|
||||
"tip", "AtomicInteger keeps the shared counter safe when many tasks run in parallel."
|
||||
));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/jwt/claims")
|
||||
public ApiResponse<Map<String, Object>> jwtClaims(@RequestHeader("Authorization") String authorization) {
|
||||
String token = authorization.substring(7);
|
||||
return ApiResponse.ok(Map.of(
|
||||
"subject", jwtUtil.username(token),
|
||||
"claims", jwtUtil.claims(token),
|
||||
"tip", "JWT claims can be parsed without a database lookup, which is one reason token auth scales well."
|
||||
));
|
||||
}
|
||||
|
||||
private List<CompletableFuture<Map<String, Object>>> createJobs(int tasks, AtomicInteger counter, ExecutorService executor) {
|
||||
return java.util.stream.IntStream.range(0, tasks)
|
||||
.mapToObj(index -> CompletableFuture.supplyAsync(() -> {
|
||||
int current = counter.incrementAndGet();
|
||||
try {
|
||||
Thread.sleep(20L + (index % 3) * 10L);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return Map.<String, Object>of(
|
||||
"task", index,
|
||||
"thread", Thread.currentThread().getName(),
|
||||
"counterAfterIncrement", current
|
||||
);
|
||||
}, executor))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> toRouteView(Map.Entry<RequestMappingInfo, HandlerMethod> entry) {
|
||||
HandlerMethod handlerMethod = entry.getValue();
|
||||
return Map.of(
|
||||
"path", entry.getKey().getPatternValues().toString(),
|
||||
"methods", entry.getKey().getMethodsCondition().getMethods().toString(),
|
||||
"handler", handlerMethod.getBeanType().getSimpleName() + "#" + handlerMethod.getMethod().getName(),
|
||||
"parameters", Arrays.stream(handlerMethod.getMethod().getParameters())
|
||||
.map(parameter -> parameter.getType().getSimpleName())
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> summarizeClass(Class<?> type) {
|
||||
List<Map<String, Object>> methods = Arrays.stream(type.getDeclaredMethods())
|
||||
.filter(method -> !method.isSynthetic())
|
||||
.map(this::toMethodView)
|
||||
.toList();
|
||||
|
||||
return Map.of(
|
||||
"className", type.getSimpleName(),
|
||||
"annotations", Arrays.stream(type.getAnnotations()).map(annotation -> annotation.annotationType().getSimpleName()).toList(),
|
||||
"methodCount", methods.size(),
|
||||
"methods", methods
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> toMethodView(Method method) {
|
||||
return Map.of(
|
||||
"name", method.getName(),
|
||||
"returnType", method.getReturnType().getSimpleName(),
|
||||
"parameterCount", method.getParameterCount(),
|
||||
"annotations", Arrays.stream(method.getAnnotations()).map(Annotation::annotationType).map(Class::getSimpleName).toList()
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> toFieldView(Field field) {
|
||||
return Map.of(
|
||||
"name", field.getName(),
|
||||
"type", field.getType().getSimpleName(),
|
||||
"annotations", Arrays.stream(field.getAnnotations()).map(Annotation::annotationType).map(Class::getSimpleName).toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,14 @@ import com.example.demo.common.ApiResponse;
|
||||
import com.example.demo.dto.auth.LoginRequest;
|
||||
import com.example.demo.security.LearningJwtUtil;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -19,27 +26,48 @@ public class LearningAuthController {
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest req) {
|
||||
// 学习演示:仅做最小账号检查
|
||||
if (!(("admin".equals(req.username()) && "admin123".equals(req.password()))
|
||||
|| ("user".equals(req.username()) && "user123".equals(req.password())))) {
|
||||
return new ApiResponse<>(401, "用户名或密码错误", null, java.time.Instant.now());
|
||||
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest request) {
|
||||
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(req.username());
|
||||
|
||||
String token = jwtUtil.generateToken(request.username());
|
||||
return ApiResponse.ok(Map.of(
|
||||
"token", token,
|
||||
"type", "Bearer",
|
||||
"username", req.username(),
|
||||
"tip", "在请求头中加入 Authorization: Bearer <token> 访问 /api/secure/**"
|
||||
"token", token,
|
||||
"type", "Bearer",
|
||||
"username", request.username(),
|
||||
"tip", "Attach Authorization: Bearer <token> when calling protected lab APIs."
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/mode")
|
||||
public ApiResponse<Map<String, Object>> mode() {
|
||||
return ApiResponse.ok(Map.of(
|
||||
"mode", "learning-jwt",
|
||||
"protectedPath", "/api/secure/**",
|
||||
"defaultAccounts", "admin/admin123, user/user123"
|
||||
"mode", "learning-jwt",
|
||||
"protectedPaths", new String[]{"/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."
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/introspect")
|
||||
public ApiResponse<Map<String, Object>> introspect(
|
||||
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization
|
||||
) {
|
||||
if (!StringUtils.hasText(authorization) || !authorization.startsWith("Bearer ")) {
|
||||
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");
|
||||
}
|
||||
|
||||
return ApiResponse.ok(Map.of(
|
||||
"subject", jwtUtil.username(token),
|
||||
"claims", jwtUtil.claims(token),
|
||||
"tip", "JWT is self-contained: the server can read claims without storing a session row for each request."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,37 +26,46 @@ public class LearningJwtFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return !request.getRequestURI().startsWith("/api/secure/");
|
||||
String uri = request.getRequestURI();
|
||||
boolean learnRoute = "/learn".equals(uri) || uri.startsWith("/learn/");
|
||||
return !(uri.startsWith("/api/secure/")
|
||||
|| uri.equals("/api/users")
|
||||
|| uri.startsWith("/api/users/")
|
||||
|| uri.startsWith("/aop/")
|
||||
|| uri.startsWith("/api/lab/")
|
||||
|| learnRoute);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
throws ServletException, IOException {
|
||||
String auth = request.getHeader("Authorization");
|
||||
if (!StringUtils.hasText(auth) || !auth.startsWith("Bearer ")) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"缺少或非法 Authorization\"}");
|
||||
writeUnauthorized(response, "Missing or invalid Authorization header");
|
||||
return;
|
||||
}
|
||||
|
||||
String token = auth.substring(7);
|
||||
if (!jwtUtil.validate(token)) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"Token 无效或过期\"}");
|
||||
writeUnauthorized(response, "Token is invalid or expired");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = jwtUtil.username(token);
|
||||
var authToken = new UsernamePasswordAuthenticationToken(
|
||||
username,
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
username,
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
);
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void writeUnauthorized(HttpServletResponse response, String message) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ public class LearningJwtUtil {
|
||||
return parse(token).getSubject();
|
||||
}
|
||||
|
||||
public Map<String, Object> claims(String token) {
|
||||
return Map.copyOf(parse(token));
|
||||
}
|
||||
|
||||
private Claims parse(String token) {
|
||||
return Jwts.parser().verifyWith(key()).build().parseSignedClaims(token).getPayload();
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ public class LearningSecurityConfig {
|
||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/", "/home", "/learn/**", "/aop/**", "/api/users/**", "/api/health",
|
||||
"/api/auth/**", "/actuator/**", "/index.html", "/users.html", "/aop.html", "/events.html"
|
||||
"/", "/home", "/api/auth/**", "/actuator/health", "/index.html", "/access.html",
|
||||
"/users.html", "/aop.html", "/events.html", "/advanced.html"
|
||||
).permitAll()
|
||||
.requestMatchers("/api/secure/**").authenticated()
|
||||
.requestMatchers("/api/secure/**", "/api/users/**", "/aop/**", "/api/lab/**", "/learn/**").authenticated()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.addFilterBefore(learningJwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
Reference in New Issue
Block a user