Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- spring security & oauth2 -->
<!-- spring security & oauth2 (Temporarily Commented Out for JustAuth Migration) -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
Expand All @@ -74,6 +75,25 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
-->

<!-- JustAuth Core & Http Client -->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.16.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里请使用默认的http请求工具

<artifactId>hutool-http</artifactId>
<version>5.8.25</version>
</dependency>
<!-- 添加 JustAuth 必须的 json 解析器 (Hutool 的依赖) -->
<dependency>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里请使用默认的jackson

<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.25</version>
</dependency>

<!-- postgresql -->
<dependency>
Expand All @@ -82,6 +102,13 @@
<scope>runtime</scope>
</dependency>

<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>

<!-- &lt;!&ndash; redis &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.involutionhell.backend.common.config;

import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

// 注册 SaToken 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 SaToken 拦截器,定义详细认证规则
registry.addInterceptor(new SaInterceptor(handler -> {
// 拦截规则配置
SaRouter
.match("/**") // 拦截所有路由
.notMatch("/api/auth/login") // 排除登录接口
.notMatch("/api/auth/register") // 排除注册接口 // 排除 Spring Boot 默认错误界面
.check(r -> StpUtil.checkLogin()); // 校验是否登录,如果未登录,这里会抛出 NotLoginException
})).addPathPatterns("/**");
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.involutionhell.backend.common.error;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import com.involutionhell.backend.common.api.ApiResponse;
import jakarta.validation.ConstraintViolationException;
import java.util.Optional;
Expand All @@ -15,30 +16,52 @@
@RestControllerAdvice
public class GlobalExceptionHandler {

// ==========================================
// Sa-Token 异常拦截
// ==========================================

/**
* 将未登录异常转换为 401 响应
*/
/**
* 将认证不足异常转换为 401 响应
* Sa-Token: 拦截未登录异常
*/
@ExceptionHandler(InsufficientAuthenticationException.class)
public ResponseEntity<ApiResponse<Void>> handleAuthentication(InsufficientAuthenticationException exception) {
@ExceptionHandler(NotLoginException.class)
public ResponseEntity<ApiResponse<Void>> handleNotLoginException(NotLoginException e) {
// 判断场景值,定制化异常信息
String message = switch (e.getType()) {
case NotLoginException.NOT_TOKEN -> "未提供 Token";
case NotLoginException.INVALID_TOKEN -> "Token 无效";
case NotLoginException.TOKEN_TIMEOUT -> "Token 已过期";
case NotLoginException.BE_REPLACED -> "Token 已被顶下线";
case NotLoginException.KICK_OUT -> "Token 已被踢下线";
case NotLoginException.TOKEN_FREEZE -> "Token 已被冻结";
case NotLoginException.NO_PREFIX -> "未按照指定前缀提交 Token";
default -> "当前会话未登录";
};
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.fail("未登录或登录状态已失效"));
.body(ApiResponse.fail(message));
}

/**
* 将权限不足异常转换为 403 响应
* Sa-Token: 拦截权限不足异常
*/
@ExceptionHandler(NotPermissionException.class)
public ResponseEntity<ApiResponse<Void>> handleNotPermissionException(NotPermissionException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.fail("拒绝访问: 缺少权限 [" + e.getCode() + "]"));
}

/**
* 将权限不足异常转换为 403 响应 (Spring Security 版)
* Sa-Token: 拦截角色不足异常
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAccessDenied(AccessDeniedException exception) {
@ExceptionHandler(NotRoleException.class)
public ResponseEntity<ApiResponse<Void>> handleNotRoleException(NotRoleException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.fail("拒绝访问: 权限不足"));
.body(ApiResponse.fail("拒绝访问: 缺少角色 [" + e.getRole() + "]"));
}

// ==========================================
// 业务与通用异常拦截
// ==========================================

/**
* 将参数校验异常转换为 400 响应
*/
Expand All @@ -65,6 +88,7 @@ public ResponseEntity<ApiResponse<Void>> handleBusiness(Exception exception) {
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception) {
exception.printStackTrace(); // 建议在开发阶段打印堆栈,生产环境应使用日志框架
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail("服务器内部错误"));
}
Expand All @@ -91,4 +115,4 @@ private String resolveValidationMessage(Exception exception) {
}
return "请求参数不合法";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.involutionhell.backend.openai.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.involutionhell.backend.openai.dto.OpenAiStreamRequest;
import com.involutionhell.backend.openai.service.OpenAiStreamService;
import jakarta.validation.Valid;
Expand All @@ -27,7 +27,7 @@ public OpenAiStreamController(OpenAiStreamService openAiStreamService) {
/**
* 调用 OpenAI Responses API 并以 SSE 形式持续推送模型输出。
*/
@PreAuthorize("isAuthenticated()")
@SaCheckLogin
@PostMapping(
path = "/responses/stream",
consumes = MediaType.APPLICATION_JSON_VALUE,
Expand All @@ -36,4 +36,4 @@ public OpenAiStreamController(OpenAiStreamService openAiStreamService) {
public SseEmitter streamResponses(@Valid @RequestBody OpenAiStreamRequest request) {
return openAiStreamService.stream(request);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
package com.involutionhell.backend.usercenter.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Value("${jwt.secret-key}")
private String secretKey;

@Bean
public JwtDecoder jwtDecoder() {
// 使用 HmacSHA256 算法生成 SecretKeySpec
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
return NimbusJwtDecoder.withSecretKey(keySpec).build();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/login", "/actuator/**", "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(withDefaults()) // 启用 OAuth2 登录支持
.oauth2Client(withDefaults()) // 启用 OAuth2 Client 支持
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // 启用 JWT 校验 (Resource Server)

return http.build();
}
}
// Temporarily commented out for JustAuth Migration

// package com.involutionhell.backend.usercenter.config;

// import org.springframework.beans.factory.annotation.Value;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
// import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
// import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
// import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
// import org.springframework.security.oauth2.jwt.JwtDecoder;
// import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
// import org.springframework.security.web.SecurityFilterChain;

// import javax.crypto.spec.SecretKeySpec;
// import java.nio.charset.StandardCharsets;

// import static org.springframework.security.config.Customizer.withDefaults;

// @Configuration
// @EnableWebSecurity
// @EnableMethodSecurity
// public class SecurityConfig {
//
// @Value("${jwt.secret-key}")
// private String secretKey;

// @Bean
// public JwtDecoder jwtDecoder() {
// // 使用 HmacSHA256 算法生成 SecretKeySpec
// SecretKeySpec keySpec = new SecretKeySpec(
// secretKey.getBytes(StandardCharsets.UTF_8),
// "HmacSHA256"
// );
// return NimbusJwtDecoder.withSecretKey(keySpec).build();
// }

// @Bean
// public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http
// .csrf(AbstractHttpConfigurer::disable)
// .authorizeHttpRequests(authorize -> authorize
// .requestMatchers("/api/auth/login", "/actuator/**", "/public/**").permitAll()
// .anyRequest().authenticated()
// )
// .oauth2Login(withDefaults()) // 启用 OAuth2 登录支持
// .oauth2Client(withDefaults()) // 启用 OAuth2 Client 支持
// .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // 启用 JWT 校验 (Resource Server)

// return http.build();
// }
// }
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.involutionhell.backend.usercenter.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.involutionhell.backend.common.api.ApiResponse;
import com.involutionhell.backend.usercenter.dto.LoginRequest;
import com.involutionhell.backend.usercenter.dto.LoginResponse;
Expand Down Expand Up @@ -37,7 +37,7 @@ public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request
/**
* 退出当前登录会话。
*/
@PreAuthorize("isAuthenticated()")
@SaCheckLogin
@PostMapping("/logout")
public ApiResponse<Void> logout() {
authService.logout();
Expand All @@ -47,9 +47,9 @@ public ApiResponse<Void> logout() {
/**
* 查询当前登录用户信息。
*/
@PreAuthorize("isAuthenticated()")
@SaCheckLogin
@GetMapping("/me")
public ApiResponse<UserView> currentUser() {
return ApiResponse.ok(authService.currentUser());
}
}
}
Loading