From 3330cd6e8d076c8577581da086e62e02847c0da7 Mon Sep 17 00:00:00 2001 From: likingcode Date: Mon, 9 Mar 2026 23:57:49 +0800 Subject: [PATCH] feat(interactive): fix scaffold startup and add auth lab page --- .../scaffold/aop/PerformanceAspect.java | 2 +- .../scaffold/controller/AuthController.java | 180 ++++++++++++++++++ .../LearningPermitAllSecurityConfig.java | 24 +++ src/main/resources/application.properties | 2 +- src/main/resources/static/advanced.html | 2 + src/main/resources/static/auth-lab.html | 141 ++++++++++++++ src/main/resources/static/index.html | 13 ++ 7 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/scaffold/controller/AuthController.java create mode 100644 src/main/java/com/example/scaffold/security/LearningPermitAllSecurityConfig.java create mode 100644 src/main/resources/static/auth-lab.html diff --git a/src/main/java/com/example/scaffold/aop/PerformanceAspect.java b/src/main/java/com/example/scaffold/aop/PerformanceAspect.java index 66f8bf6..accbe99 100644 --- a/src/main/java/com/example/scaffold/aop/PerformanceAspect.java +++ b/src/main/java/com/example/scaffold/aop/PerformanceAspect.java @@ -27,7 +27,7 @@ public class PerformanceAspect { public final AtomicLong errorCount = new AtomicLong(); } - @Around("execution(* com.example.scaffold..*.*(..))") + @Around("execution(* com.example.scaffold..*.*(..)) && !within(com.example.scaffold.security..*) && !within(org.springframework.web.filter..*)") public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable { String key = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName(); long startTime = System.nanoTime(); diff --git a/src/main/java/com/example/scaffold/controller/AuthController.java b/src/main/java/com/example/scaffold/controller/AuthController.java new file mode 100644 index 0000000..9af833e --- /dev/null +++ b/src/main/java/com/example/scaffold/controller/AuthController.java @@ -0,0 +1,180 @@ +package com.example.scaffold.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.example.scaffold.security.jwt.JwtUtil; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 认证控制器 - 支持 JWT 和 Sa-Token + * + * 学习要点: + * 1. 登录认证流程 + * 2. Token 生成与刷新 + * 3. 登出处理 + */ +@Slf4j +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public AuthController(JwtUtil jwtUtil, ObjectProvider authenticationManagerProvider) { + this.jwtUtil = jwtUtil; + this.authenticationManager = authenticationManagerProvider.getIfAvailable(); + } + + @Value("${auth.type:none}") + private String authType; + + /** + * 登录 - 支持多种认证方式 + */ + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + log.info("🔐 用户登录: username={}, authType={}", request.getUsername(), authType); + + return switch (authType) { + case "jwt" -> jwtLogin(request); + case "satoken" -> saTokenLogin(request); + default -> demoLogin(request); + }; + } + + /** + * JWT 登录 + */ + private ResponseEntity jwtLogin(LoginRequest request) { + if (authenticationManager == null) { + return ResponseEntity.status(503).body(Map.of( + "success", false, + "message", "当前 profile 未启用 JWT AuthenticationManager,请切到 advanced 或 auth.type=jwt" + )); + } + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getUsername(), + request.getPassword() + ) + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 生成 JWT Token + String token = jwtUtil.generateToken(1L, request.getUsername(), "USER"); + + return ResponseEntity.ok(Map.of( + "success", true, + "token", token, + "type", "Bearer", + "username", request.getUsername() + )); + } catch (Exception e) { + return ResponseEntity.status(401).body(Map.of( + "success", false, + "message", "用户名或密码错误" + )); + } + } + + /** + * Sa-Token 登录 + */ + private ResponseEntity saTokenLogin(LoginRequest request) { + // 简单校验(实际应从数据库校验) + if ("admin".equals(request.getUsername()) && "admin123".equals(request.getPassword())) { + StpUtil.login(10001); + return ResponseEntity.ok(Map.of( + "success", true, + "token", StpUtil.getTokenValue(), + "type", "satoken", + "username", request.getUsername() + )); + } + + return ResponseEntity.status(401).body(Map.of( + "success", false, + "message", "用户名或密码错误" + )); + } + + /** + * 演示登录(无认证) + */ + private ResponseEntity demoLogin(LoginRequest request) { + return ResponseEntity.ok(Map.of( + "success", true, + "message", "演示模式,无需认证", + "username", request.getUsername() + )); + } + + /** + * 登出 + */ + @PostMapping("/logout") + public ResponseEntity logout() { + if ("satoken".equals(authType)) { + StpUtil.logout(); + } + SecurityContextHolder.clearContext(); + return ResponseEntity.ok(Map.of("success", true, "message", "登出成功")); + } + + /** + * 获取当前登录用户信息 + */ + @GetMapping("/info") + public ResponseEntity getCurrentUser() { + if ("satoken".equals(authType) && StpUtil.isLogin()) { + return ResponseEntity.ok(Map.of( + "loggedIn", true, + "userId", StpUtil.getLoginId(), + "authType", authType + )); + } + + return ResponseEntity.ok(Map.of( + "loggedIn", false, + "authType", authType + )); + } + + /** + * 刷新 Token(JWT) + */ + @PostMapping("/refresh") + public ResponseEntity refreshToken(@RequestHeader("Authorization") String authHeader) { + if (!authHeader.startsWith("Bearer ")) { + return ResponseEntity.badRequest().body(Map.of("error", "无效的 Token 格式")); + } + + String token = authHeader.substring(7); + if (!jwtUtil.validateToken(token)) { + return ResponseEntity.status(401).body(Map.of("error", "Token 无效或已过期")); + } + + String newToken = jwtUtil.refreshToken(token); + return ResponseEntity.ok(Map.of("token", newToken)); + } + + @Data + public static class LoginRequest { + private String username; + private String password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/security/LearningPermitAllSecurityConfig.java b/src/main/java/com/example/scaffold/security/LearningPermitAllSecurityConfig.java new file mode 100644 index 0000000..7714e19 --- /dev/null +++ b/src/main/java/com/example/scaffold/security/LearningPermitAllSecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.scaffold.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@ConditionalOnProperty(name = "auth.type", havingValue = "none", matchIfMissing = true) +public class LearningPermitAllSecurityConfig { + + @Bean + public SecurityFilterChain permitAllSecurityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())); + return http.build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ce2658f..47a110b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.application.name=springboot-scaffold -server.port=8082 +server.port=8083 spring.profiles.active=${APP_PROFILE:learn} diff --git a/src/main/resources/static/advanced.html b/src/main/resources/static/advanced.html index 470db8d..a8bdeae 100644 --- a/src/main/resources/static/advanced.html +++ b/src/main/resources/static/advanced.html @@ -60,6 +60,7 @@ 🔄 事务 👥 用户 🚀 高级 + 🔐 鉴权实验室 🔌 API @@ -84,6 +85,7 @@

🔐 认证方案对比

+ 进入鉴权实验室
diff --git a/src/main/resources/static/auth-lab.html b/src/main/resources/static/auth-lab.html new file mode 100644 index 0000000..74d1101 --- /dev/null +++ b/src/main/resources/static/auth-lab.html @@ -0,0 +1,141 @@ + + + + + + 鉴权实验室 - Spring Boot + + + +
+
+

🔐 Spring 鉴权实验室

+

登录 → 取 Token → 访问受保护接口 → 对比 learn/advanced 模式

+
+ + + +
+ 实验任务卡 +
    +
  • 步骤1:点击“读取当前模式”,确认现在是 learn / advanced
  • +
  • 步骤2:用 admin/admin123 登录,拿到 token
  • +
  • 步骤3:带上 token 调用当前实验接口,观察是否放行或返回 503/401
  • +
  • 步骤4:切换 auth.type 后对比 none/jwt/satoken 行为差异
  • +
+
+ +
+

当前模式

+
+ +
+
点击按钮读取...
+
+ +
+

登录拿 Token

+
+ + + +
+
演示账号:admin/admin123 或 user/user123
+
点击登录后显示响应...
+
+ +
+

接口访问实验

+
+ + +
+
当前页面会优先从 localStorage 读取 token 并自动加到 Authorization 头里。learn 模式下接口通常直接放行;advanced+jwt 下更适合观察真实鉴权链路。
+
点击按钮后显示接口结果...
+
+
+ + + diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index d1e5dfb..9c2aae6 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -98,6 +98,7 @@ 🔄 事务管理 👥 用户管理 🚀 高级功能 + 🔐 鉴权实验室 🔌 API 测试 @@ -175,6 +176,18 @@ 进阶学习 → +
+

🔐 鉴权实验室

+

一步步体验登录、带 Token 请求、鉴权放行/拦截,对比 learn / advanced 模式差异。

+
    +
  • 登录拿 Token
  • +
  • 自动携带 Authorization
  • +
  • 当前模式读取
  • +
  • 交互式错误观察
  • +
+ 开始实验 → +
+

🔌 API 测试面板

在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。