feat: finish bilingual learning auth cockpit
This commit is contained in:
@@ -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."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user