feat(interactive): fix scaffold startup and add auth lab page
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
spring.application.name=springboot-scaffold
|
||||
server.port=8082
|
||||
server.port=8083
|
||||
|
||||
spring.profiles.active=${APP_PROFILE:learn}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<a href="transaction.html">🔄 事务</a>
|
||||
<a href="users.html">👥 用户</a>
|
||||
<a href="advanced.html" class="active">🚀 高级</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="api.html">🔌 API</a>
|
||||
</div>
|
||||
|
||||
@@ -84,6 +85,7 @@
|
||||
<div class="card">
|
||||
<h3>🔐 认证方案对比</h3>
|
||||
<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>
|
||||
|
||||
|
||||
141
src/main/resources/static/auth-lab.html
Normal file
141
src/main/resources/static/auth-lab.html
Normal 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>
|
||||
@@ -98,6 +98,7 @@
|
||||
<a href="transaction.html">🔄 事务管理</a>
|
||||
<a href="users.html">👥 用户管理</a>
|
||||
<a href="advanced.html">🚀 高级功能</a>
|
||||
<a href="auth-lab.html">🔐 鉴权实验室</a>
|
||||
<a href="api.html">🔌 API 测试</a>
|
||||
</div>
|
||||
|
||||
@@ -175,6 +176,18 @@
|
||||
<a href="advanced.html" class="btn">进阶学习 →</a>
|
||||
</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">
|
||||
<h3>🔌 API 测试面板</h3>
|
||||
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
|
||||
|
||||
Reference in New Issue
Block a user