feat(interactive): fix scaffold startup and add auth lab page

This commit is contained in:
likingcode
2026-03-09 23:57:49 +08:00
parent f846616e0b
commit 3330cd6e8d
7 changed files with 362 additions and 2 deletions

View File

@@ -27,7 +27,7 @@ public class PerformanceAspect {
public final AtomicLong errorCount = new AtomicLong(); 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 { public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String key = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName(); String key = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
long startTime = System.nanoTime(); long startTime = System.nanoTime();

View File

@@ -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<AuthenticationManager> 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
));
}
/**
* 刷新 TokenJWT
*/
@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;
}
}

View File

@@ -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();
}
}

View File

@@ -1,5 +1,5 @@
spring.application.name=springboot-scaffold spring.application.name=springboot-scaffold
server.port=8082 server.port=8083
spring.profiles.active=${APP_PROFILE:learn} spring.profiles.active=${APP_PROFILE:learn}

View File

@@ -60,6 +60,7 @@
<a href="transaction.html">🔄 事务</a> <a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a> <a href="users.html">👥 用户</a>
<a href="advanced.html" class="active">🚀 高级</a> <a href="advanced.html" class="active">🚀 高级</a>
<a href="auth-lab.html">🔐 鉴权实验室</a>
<a href="api.html">🔌 API</a> <a href="api.html">🔌 API</a>
</div> </div>
@@ -84,6 +85,7 @@
<div class="card"> <div class="card">
<h3>🔐 认证方案对比</h3> <h3>🔐 认证方案对比</h3>
<button class="btn" onclick="loadAuthCompare()">JWT vs Sa-Token</button> <button class="btn" onclick="loadAuthCompare()">JWT vs Sa-Token</button>
<a class="btn btn-secondary" href="auth-lab.html">进入鉴权实验室</a>
<div id="authResult" class="result"></div> <div id="authResult" class="result"></div>
</div> </div>

View File

@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鉴权实验室 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#f5f5f5; }
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg,#4facfe,#00f2fe); color:#fff; padding:28px 20px; border-radius:12px; margin-bottom:20px; text-align:center; }
.nav { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-bottom:20px; }
.nav a { padding:10px 18px; background:#fff; border-radius:20px; text-decoration:none; color:#333; }
.nav a.active { background:#4facfe; color:#fff; }
.card { background:#fff; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 10px rgba(0,0,0,.08); }
.card h3 { color:#1890ff; margin-bottom:12px; }
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
.row { display:flex; gap:10px; flex-wrap:wrap; margin:10px 0; }
input, select { padding:10px; border:1px solid #ddd; border-radius:6px; min-width:200px; }
button { padding:10px 16px; background:#1890ff; color:#fff; border:none; border-radius:6px; cursor:pointer; }
button.secondary { background:#6c757d; }
.result { background:#111827; color:#d1d5db; padding:14px; border-radius:8px; min-height:120px; white-space:pre-wrap; font-family:monospace; }
.hint { color:#666; margin-top:8px; line-height:1.7; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 Spring 鉴权实验室</h1>
<p>登录 → 取 Token → 访问受保护接口 → 对比 learn/advanced 模式</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="advanced.html">🚀 高级功能</a>
<a href="api.html">🔌 API 测试</a>
<a href="auth-lab.html" class="active">🔐 鉴权实验室</a>
</div>
<div class="lab">
<strong>实验任务卡</strong>
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
<li>步骤1点击“读取当前模式”确认现在是 learn / advanced</li>
<li>步骤2用 admin/admin123 登录,拿到 token</li>
<li>步骤3带上 token 调用当前实验接口,观察是否放行或返回 503/401</li>
<li>步骤4切换 auth.type 后对比 none/jwt/satoken 行为差异</li>
</ul>
</div>
<div class="card">
<h3>当前模式</h3>
<div class="row">
<button onclick="loadProfile()">读取当前模式</button>
</div>
<div id="profileResult" class="result">点击按钮读取...</div>
</div>
<div class="card">
<h3>登录拿 Token</h3>
<div class="row">
<input id="username" value="admin" placeholder="用户名">
<input id="password" value="admin123" placeholder="密码" type="password">
<button onclick="login()">登录</button>
</div>
<div class="hint">演示账号admin/admin123 或 user/user123</div>
<div id="loginResult" class="result">点击登录后显示响应...</div>
</div>
<div class="card">
<h3>接口访问实验</h3>
<div class="row">
<button onclick="callSecureApi()">访问 /api/auth/info</button>
<button class="secondary" onclick="clearToken()">清空本地 Token</button>
</div>
<div class="hint">当前页面会优先从 localStorage 读取 token 并自动加到 Authorization 头里。learn 模式下接口通常直接放行advanced+jwt 下更适合观察真实鉴权链路。</div>
<div id="secureResult" class="result">点击按钮后显示接口结果...</div>
</div>
</div>
<script>
let cachedToken = localStorage.getItem('spring.scaffold.token') || '';
async function loadProfile() {
const el = document.getElementById('profileResult');
el.textContent = '读取中...';
try {
const res = await fetch('/api/profile');
const data = await res.json();
el.textContent = JSON.stringify(data, null, 2);
} catch (e) {
el.textContent = '读取失败: ' + e.message;
}
}
async function login() {
const el = document.getElementById('loginResult');
el.textContent = '登录中...';
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value,
})
});
const data = await res.json();
const token = data.token || data.data?.token || '';
if (token) {
cachedToken = token;
localStorage.setItem('spring.scaffold.token', token);
}
el.textContent = JSON.stringify(data, null, 2);
} catch (e) {
el.textContent = '登录失败: ' + e.message;
}
}
async function callSecureApi() {
const el = document.getElementById('secureResult');
el.textContent = '请求中...';
try {
const headers = {};
if (cachedToken) headers['Authorization'] = 'Bearer ' + cachedToken;
const res = await fetch('/api/auth/info', { headers });
const text = await res.text();
el.textContent = `HTTP ${res.status}\n\n${text}`;
} catch (e) {
el.textContent = '请求失败: ' + e.message;
}
}
function clearToken() {
cachedToken = '';
localStorage.removeItem('spring.scaffold.token');
document.getElementById('secureResult').textContent = '本地 token 已清空';
}
loadProfile();
</script>
</body>
</html>

View File

@@ -98,6 +98,7 @@
<a href="transaction.html">🔄 事务管理</a> <a href="transaction.html">🔄 事务管理</a>
<a href="users.html">👥 用户管理</a> <a href="users.html">👥 用户管理</a>
<a href="advanced.html">🚀 高级功能</a> <a href="advanced.html">🚀 高级功能</a>
<a href="auth-lab.html">🔐 鉴权实验室</a>
<a href="api.html">🔌 API 测试</a> <a href="api.html">🔌 API 测试</a>
</div> </div>
@@ -175,6 +176,18 @@
<a href="advanced.html" class="btn">进阶学习 →</a> <a href="advanced.html" class="btn">进阶学习 →</a>
</div> </div>
<div class="card">
<h3>🔐 鉴权实验室</h3>
<p>一步步体验登录、带 Token 请求、鉴权放行/拦截,对比 learn / advanced 模式差异。</p>
<ul class="feature-list">
<li><span class="icon"></span>登录拿 Token</li>
<li><span class="icon"></span>自动携带 Authorization</li>
<li><span class="icon"></span>当前模式读取</li>
<li><span class="icon"></span>交互式错误观察</li>
</ul>
<a href="auth-lab.html" class="btn">开始实验 →</a>
</div>
<div class="card"> <div class="card">
<h3>🔌 API 测试面板</h3> <h3>🔌 API 测试面板</h3>
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p> <p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>