feat(learning-auth): add optional JWT learning flow with secure demo endpoint

This commit is contained in:
likingcode
2026-03-09 02:11:49 +08:00
parent 8f93488989
commit efcfe7e012
11 changed files with 322 additions and 1 deletions

View File

@@ -45,7 +45,9 @@ public class LearnController {
"POST /learn/body - JSON 请求体示例",
"GET /learn/path/{id} - 路径变量示例",
"GET /learn/header - 请求头示例",
"GET /learn/cookie - Cookie 示例"
"GET /learn/cookie - Cookie 示例",
"POST /api/auth/login - 学习用 JWT 登录",
"GET /api/secure/me - 受保护接口(需 Bearer Token"
});
return info;
}

View File

@@ -0,0 +1,45 @@
package com.example.demo.controller.auth;
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 java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class LearningAuthController {
private final LearningJwtUtil jwtUtil;
public LearningAuthController(LearningJwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@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());
}
String token = jwtUtil.generateToken(req.username());
return ApiResponse.ok(Map.of(
"token", token,
"type", "Bearer",
"username", req.username(),
"tip", "在请求头中加入 Authorization: Bearer <token> 访问 /api/secure/**"
));
}
@GetMapping("/mode")
public ApiResponse<Map<String, Object>> mode() {
return ApiResponse.ok(Map.of(
"mode", "learning-jwt",
"protectedPath", "/api/secure/**",
"defaultAccounts", "admin/admin123, user/user123"
));
}
}

View File

@@ -0,0 +1,23 @@
package com.example.demo.controller.auth;
import com.example.demo.common.ApiResponse;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/secure")
public class SecureDemoController {
@GetMapping("/me")
public ApiResponse<Map<String, Object>> me(Authentication authentication) {
return ApiResponse.ok(Map.of(
"principal", authentication.getName(),
"authorities", authentication.getAuthorities(),
"message", "你已通过学习用 JWT 鉴权"
));
}
}

View File

@@ -0,0 +1,8 @@
package com.example.demo.dto.auth;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@NotBlank(message = "用户名不能为空") String username,
@NotBlank(message = "密码不能为空") String password
) {}

View File

@@ -0,0 +1,62 @@
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class LearningJwtFilter extends OncePerRequestFilter {
private final LearningJwtUtil jwtUtil;
public LearningJwtFilter(LearningJwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !request.getRequestURI().startsWith("/api/secure/");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
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\"}");
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 无效或过期\"}");
return;
}
String username = jwtUtil.username(token);
var authToken = new UsernamePasswordAuthenticationToken(
username,
null,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.demo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
@Component
public class LearningJwtUtil {
@Value("${learning.auth.jwt.secret}")
private String secret;
@Value("${learning.auth.jwt.expiration:86400000}")
private long expiration;
private SecretKey key() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(String username) {
Date now = new Date();
return Jwts.builder()
.claims(Map.of("username", username))
.subject(username)
.issuedAt(now)
.expiration(new Date(now.getTime() + expiration))
.signWith(key(), Jwts.SIG.HS256)
.compact();
}
public boolean validate(String token) {
try {
parse(token);
return true;
} catch (Exception e) {
return false;
}
}
public String username(String token) {
return parse(token).getSubject();
}
private Claims parse(String token) {
return Jwts.parser().verifyWith(key()).build().parseSignedClaims(token).getPayload();
}
}

View File

@@ -0,0 +1,38 @@
package com.example.demo.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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@ConditionalOnProperty(name = "learning.auth.enabled", havingValue = "true", matchIfMissing = true)
public class LearningSecurityConfig {
private final LearningJwtFilter learningJwtFilter;
public LearningSecurityConfig(LearningJwtFilter learningJwtFilter) {
this.learningJwtFilter = learningJwtFilter;
}
@Bean
public SecurityFilterChain learningSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.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"
).permitAll()
.requestMatchers("/api/secure/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(learningJwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}