有循环依赖
The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | jwtAuthenticationFilter defined in file [D:\work\java-space\deploy-ease-platform\backend\target\classes\com\qqchen\deploy\backend\common\security\filter\JwtAuthenticationFilter.class] ↑ ↓ | securityConfig defined in file [D:\work\java-space\deploy-ease-platform\backend\target\classes\com\qqchen\deploy\backend\common\security\config\SecurityConfig.class] └─────┘
This commit is contained in:
parent
68d42ce6f0
commit
06aaead053
@ -24,6 +24,8 @@
|
|||||||
<lombok.version>1.18.30</lombok.version>
|
<lombok.version>1.18.30</lombok.version>
|
||||||
<spring-security.version>6.2.0</spring-security.version>
|
<spring-security.version>6.2.0</spring-security.version>
|
||||||
<jjwt.version>0.12.3</jjwt.version>
|
<jjwt.version>0.12.3</jjwt.version>
|
||||||
|
<springdoc.version>2.2.0</springdoc.version>
|
||||||
|
<hutool.version>5.8.23</hutool.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -140,6 +142,18 @@
|
|||||||
<version>${jjwt.version}</version>
|
<version>${jjwt.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Swagger -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>${springdoc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Hutool -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>${hutool.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import com.qqchen.deploy.backend.common.controller.BaseController;
|
|||||||
import com.qqchen.deploy.backend.common.api.Response;
|
import com.qqchen.deploy.backend.common.api.Response;
|
||||||
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
import com.qqchen.deploy.backend.converter.UserConverter;
|
import com.qqchen.deploy.backend.converter.UserConverter;
|
||||||
|
import com.qqchen.deploy.backend.dto.request.LoginRequest;
|
||||||
|
import com.qqchen.deploy.backend.dto.response.LoginResponse;
|
||||||
import com.qqchen.deploy.backend.entity.User;
|
import com.qqchen.deploy.backend.entity.User;
|
||||||
import com.qqchen.deploy.backend.dto.query.UserQuery;
|
import com.qqchen.deploy.backend.dto.query.UserQuery;
|
||||||
import com.qqchen.deploy.backend.dto.request.UserRequest;
|
import com.qqchen.deploy.backend.dto.request.UserRequest;
|
||||||
import com.qqchen.deploy.backend.dto.response.UserResponse;
|
import com.qqchen.deploy.backend.dto.response.UserResponse;
|
||||||
import com.qqchen.deploy.backend.dto.request.UserRegisterRequest;
|
import com.qqchen.deploy.backend.dto.request.UserRegisterRequest;
|
||||||
import com.qqchen.deploy.backend.service.IUserService;
|
import com.qqchen.deploy.backend.service.IUserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@ -29,13 +32,18 @@ public class UserApiController extends BaseController<User, Long, UserQuery, Use
|
|||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "用户登录")
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Response<LoginResponse> login(@Validated @RequestBody LoginRequest request) {
|
||||||
|
return Response.success(userService.login(request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public Response<UserResponse> register(@Validated @RequestBody UserRegisterRequest request) {
|
public Response<UserResponse> register(@Validated @RequestBody UserRegisterRequest request) {
|
||||||
// 基础的注册逻辑
|
// 基础的注册逻辑
|
||||||
if (!request.getPassword().equals(request.getConfirmPassword())) {
|
if (!request.getPassword().equals(request.getConfirmPassword())) {
|
||||||
return Response.error(ResponseCode.USER_NOT_FOUND);
|
return Response.error(ResponseCode.USER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = converter.toEntity(request);
|
User user = converter.toEntity(request);
|
||||||
User savedUser = userService.register(user);
|
User savedUser = userService.register(user);
|
||||||
return Response.success(converter.toResponse(savedUser));
|
return Response.success(converter.toResponse(savedUser));
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface TenantFilter {
|
||||||
|
/**
|
||||||
|
* 是否启用租户过滤
|
||||||
|
*/
|
||||||
|
boolean value() default true;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||||
|
|
||||||
|
javaTimeModule.addSerializer(LocalDateTime.class,
|
||||||
|
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
|
||||||
|
|
||||||
|
javaTimeModule.addDeserializer(LocalDateTime.class,
|
||||||
|
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
|
||||||
|
|
||||||
|
objectMapper.registerModule(javaTimeModule);
|
||||||
|
|
||||||
|
return objectMapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.context;
|
||||||
|
|
||||||
|
public class TenantContext {
|
||||||
|
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void setTenantId(String tenantId) {
|
||||||
|
CURRENT_TENANT.set(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTenantId() {
|
||||||
|
return CURRENT_TENANT.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
CURRENT_TENANT.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSystemTenant() {
|
||||||
|
String tenantId = getTenantId();
|
||||||
|
return tenantId == null || "0".equals(tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,9 +13,10 @@ public enum ResponseCode {
|
|||||||
CONFLICT(409, "response.conflict"),
|
CONFLICT(409, "response.conflict"),
|
||||||
|
|
||||||
// 业务错误码
|
// 业务错误码
|
||||||
USER_NOT_FOUND(1001, "user.not.found"),
|
TENANT_NOT_FOUND(1001, "tenant.not.found"),
|
||||||
USERNAME_EXISTS(1002, "user.username.exists"),
|
USER_NOT_FOUND(2001, "user.not.found"),
|
||||||
EMAIL_EXISTS(1003, "user.email.exists");
|
USERNAME_EXISTS(2002, "user.username.exists"),
|
||||||
|
EMAIL_EXISTS(2003, "user.email.exists");
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public abstract class DomainEvent {
|
public abstract class DomainEvent {
|
||||||
|
|
||||||
private final String eventId;
|
private final String eventId;
|
||||||
|
|
||||||
private final LocalDateTime occurredOn;
|
private final LocalDateTime occurredOn;
|
||||||
|
|
||||||
protected DomainEvent() {
|
protected DomainEvent() {
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.exception;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.common.utils.MessageUtils;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private final ResponseCode errorCode;
|
||||||
|
|
||||||
|
public BusinessException(ResponseCode errorCode) {
|
||||||
|
super(MessageUtils.getMessage(errorCode.getMessageKey()));
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BusinessException(ResponseCode errorCode, Throwable cause) {
|
||||||
|
super(MessageUtils.getMessage(errorCode.getMessageKey()), cause);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.exception;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
// @ExceptionHandler(AuthenticationException.class)
|
||||||
|
// public ApiResult<String> handleAuthenticationException(AuthenticationException e) {
|
||||||
|
// log.error("认证异常", e);
|
||||||
|
// if (e instanceof BadCredentialsException) {
|
||||||
|
// return ApiResult.error(401, "用户名或密码错误");
|
||||||
|
// }
|
||||||
|
// return ApiResult.error(401, "认证失败:" + e.getMessage());
|
||||||
|
// }
|
||||||
|
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public Response<?> handleApiException(BusinessException e) {
|
||||||
|
return Response.error(e.getErrorCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public Response<?> handleException(Exception e) {
|
||||||
|
log.error("系统错误", e);
|
||||||
|
return Response.error(ResponseCode.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.filter;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.context.TenantContext;
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(1)
|
||||||
|
public class TenantFilter implements Filter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
HttpServletRequest req = (HttpServletRequest) request;
|
||||||
|
try {
|
||||||
|
// 从请求头中获取租户ID
|
||||||
|
String tenantId = req.getHeader("X-Tenant-ID");
|
||||||
|
if (tenantId != null) {
|
||||||
|
TenantContext.setTenantId(tenantId);
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
// 清理租户上下文
|
||||||
|
TenantContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.interceptor;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.context.TenantContext;
|
||||||
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.common.exception.BusinessException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TenantInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||||
|
|
||||||
|
// 不需要进行租户隔离的路径
|
||||||
|
private final List<String> excludePaths = Arrays.asList(
|
||||||
|
"/api/v1/users/login",
|
||||||
|
"/api/system/tenants/list",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/v3/api-docs/**"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
// 如果是白名单路径,不进行租户校验
|
||||||
|
if (isExcludePath(path)) {
|
||||||
|
TenantContext.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 获取租户ID
|
||||||
|
String tenantId = request.getHeader("X-Tenant-ID");
|
||||||
|
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||||
|
throw new BusinessException(ResponseCode.TENANT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
TenantContext.setTenantId(tenantId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||||
|
TenantContext.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExcludePath(String path) {
|
||||||
|
return excludePaths.stream()
|
||||||
|
.anyMatch(pattern -> pathMatcher.match(pattern, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package com.qqchen.deploy.backend.common.security.config;
|
package com.qqchen.deploy.backend.common.security.config;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.security.filter.JwtAuthenticationFilter;
|
||||||
|
import com.qqchen.deploy.backend.common.security.handler.CustomAuthenticationEntryPoint;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
@ -14,6 +17,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
@ -22,8 +26,13 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
|
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
@ -31,10 +40,14 @@ public class SecurityConfig {
|
|||||||
.csrf(csrf -> csrf.disable()) // 禁用CSRF
|
.csrf(csrf -> csrf.disable()) // 禁用CSRF
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session
|
||||||
|
.exceptionHandling(exceptions -> exceptions
|
||||||
|
.authenticationEntryPoint(authenticationEntryPoint)
|
||||||
|
)
|
||||||
.formLogin(form -> form.disable()) // 禁用form登录
|
.formLogin(form -> form.disable()) // 禁用form登录
|
||||||
.httpBasic(basic -> basic.disable()) // 禁用basic认证
|
.httpBasic(basic -> basic.disable()) // 禁用basic认证
|
||||||
.logout(logout -> logout.disable()) // 禁用logout
|
.logout(logout -> logout.disable()) // 禁用logout
|
||||||
.anonymous(anonymous -> {}) // 允许匿名访问
|
.anonymous(anonymous -> {
|
||||||
|
}) // 允许匿名访问
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// 公开接口
|
// 公开接口
|
||||||
// .requestMatchers("/api/v1/users/register").permitAll()
|
// .requestMatchers("/api/v1/users/register").permitAll()
|
||||||
@ -47,7 +60,8 @@ public class SecurityConfig {
|
|||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
// 生产环境建议改为需要认证
|
// 生产环境建议改为需要认证
|
||||||
//.anyRequest().authenticated()
|
//.anyRequest().authenticated()
|
||||||
);
|
|
||||||
|
).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -23,13 +25,25 @@ import java.io.IOException;
|
|||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtTokenUtil jwtTokenUtil;
|
private final JwtTokenUtil jwtTokenUtil;
|
||||||
|
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
// 添加白名单路径
|
||||||
|
private static final List<String> WHITELIST = Arrays.asList(
|
||||||
|
"/api/v1/users/login",
|
||||||
|
"/api/v1/users/register",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/v3/api-docs/**"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = request.getServletPath();
|
||||||
|
return WHITELIST.stream().anyMatch(path::startsWith);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String authHeader = request.getHeader("Authorization");
|
String authHeader = request.getHeader("Authorization");
|
||||||
String token = null;
|
String token = null;
|
||||||
@ -39,10 +53,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
token = authHeader.substring(7);
|
token = authHeader.substring(7);
|
||||||
username = jwtTokenUtil.getUsernameFromToken(token);
|
username = jwtTokenUtil.getUsernameFromToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
if (jwtTokenUtil.validateToken(token, userDetails)) {
|
if (jwtTokenUtil.validateToken(token, userDetails)) {
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, null, userDetails.getAuthorities());
|
userDetails, null, userDetails.getAuthorities());
|
||||||
@ -50,10 +62,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Cannot set user authentication", e);
|
log.error("Cannot set user authentication", e);
|
||||||
|
chain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.qqchen.deploy.backend.common.security.handler;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.qqchen.deploy.backend.common.api.Response;
|
||||||
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.common.utils.MessageUtils;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
Response<?> result;
|
||||||
|
if (authException instanceof BadCredentialsException) {
|
||||||
|
result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.SUCCESS.getMessageKey()));
|
||||||
|
} else if (authException instanceof InternalAuthenticationServiceException) {
|
||||||
|
result = Response.error(ResponseCode.USER_NOT_FOUND, MessageUtils.getMessage(ResponseCode.USER_NOT_FOUND.getMessageKey()));
|
||||||
|
} else {
|
||||||
|
result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.SUCCESS.getMessageKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.qqchen.deploy.backend.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoginRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.qqchen.deploy.backend.dto.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class LoginResponse extends UserResponse {
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.qqchen.deploy.backend.service;
|
package com.qqchen.deploy.backend.service;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.common.service.IBaseService;
|
import com.qqchen.deploy.backend.common.service.IBaseService;
|
||||||
|
import com.qqchen.deploy.backend.dto.request.LoginRequest;
|
||||||
|
import com.qqchen.deploy.backend.dto.response.LoginResponse;
|
||||||
import com.qqchen.deploy.backend.entity.User;
|
import com.qqchen.deploy.backend.entity.User;
|
||||||
|
|
||||||
public interface IUserService extends IBaseService<User, Long> {
|
public interface IUserService extends IBaseService<User, Long> {
|
||||||
@ -8,4 +10,6 @@ public interface IUserService extends IBaseService<User, Long> {
|
|||||||
User findByUsername(String username);
|
User findByUsername(String username);
|
||||||
boolean checkUsernameExists(String username);
|
boolean checkUsernameExists(String username);
|
||||||
boolean checkEmailExists(String email);
|
boolean checkEmailExists(String email);
|
||||||
|
|
||||||
|
LoginResponse login(LoginRequest request);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.qqchen.deploy.backend.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.entity.User;
|
||||||
|
import com.qqchen.deploy.backend.repository.IUserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
|
private final IUserRepository userRepository; // 直接使用Repository
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
// 直接查询数据库,不要通过Controller或其他Service
|
||||||
|
return userRepository.findByUsernameAndDeletedFalse(username)
|
||||||
|
.map(this::createUserDetails)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDetails createUserDetails(User user) {
|
||||||
|
return org.springframework.security.core.userdetails.User
|
||||||
|
.withUsername(user.getUsername())
|
||||||
|
.password(user.getPassword())
|
||||||
|
.authorities(Collections.emptyList()) // 或者从数据库加载权限
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,35 @@
|
|||||||
package com.qqchen.deploy.backend.service.impl;
|
package com.qqchen.deploy.backend.service.impl;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.common.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.common.exception.BusinessException;
|
||||||
|
import com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
|
||||||
import com.qqchen.deploy.backend.common.service.impl.BaseServiceImpl;
|
import com.qqchen.deploy.backend.common.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.dto.request.LoginRequest;
|
||||||
|
import com.qqchen.deploy.backend.dto.response.LoginResponse;
|
||||||
import com.qqchen.deploy.backend.entity.User;
|
import com.qqchen.deploy.backend.entity.User;
|
||||||
import com.qqchen.deploy.backend.repository.IUserRepository;
|
import com.qqchen.deploy.backend.repository.IUserRepository;
|
||||||
import com.qqchen.deploy.backend.service.IUserService;
|
import com.qqchen.deploy.backend.service.IUserService;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Slf4j
|
||||||
public class UserServiceImpl extends BaseServiceImpl<User, Long> implements IUserService {
|
public class UserServiceImpl extends BaseServiceImpl<User, Long> implements IUserService {
|
||||||
|
|
||||||
private final IUserRepository userRepository;
|
private final IUserRepository userRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private JwtTokenUtil jwtTokenUtil;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public UserServiceImpl(IUserRepository userRepository) {
|
public UserServiceImpl(IUserRepository userRepository) {
|
||||||
super(userRepository);
|
super(userRepository);
|
||||||
@ -47,4 +63,24 @@ public class UserServiceImpl extends BaseServiceImpl<User, Long> implements IUse
|
|||||||
return userRepository.existsByEmail(email);
|
return userRepository.existsByEmail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginResponse login(LoginRequest request) {
|
||||||
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||||
|
);
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
String token = jwtTokenUtil.generateToken(userDetails);
|
||||||
|
User user = userRepository.findByUsernameAndDeletedFalse(userDetails.getUsername()).orElseThrow(() -> new BusinessException(ResponseCode.USER_NOT_FOUND));
|
||||||
|
LoginResponse response = new LoginResponse();
|
||||||
|
response.setId(user.getId());
|
||||||
|
response.setUsername(user.getUsername());
|
||||||
|
response.setNickname(user.getNickname());
|
||||||
|
response.setEmail(user.getEmail());
|
||||||
|
response.setPhone(user.getPhone());
|
||||||
|
response.setToken(token);
|
||||||
|
log.info("用户 {} ({}) 登录成功", user.getUsername(), user.getNickname());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ spring:
|
|||||||
jdbc:
|
jdbc:
|
||||||
time_zone: Asia/Shanghai
|
time_zone: Asia/Shanghai
|
||||||
mvc:
|
mvc:
|
||||||
log-request-details: true # \u6253\u5370\u8BF7\u6C42\u8BE6\u60C5
|
log-request-details: true
|
||||||
messages:
|
messages:
|
||||||
basename: messages
|
basename: messages
|
||||||
encoding: UTF-8
|
encoding: UTF-8
|
||||||
@ -35,8 +35,8 @@ logging:
|
|||||||
level:
|
level:
|
||||||
org.springframework.web: DEBUG
|
org.springframework.web: DEBUG
|
||||||
org.springframework.context.i18n: DEBUG
|
org.springframework.context.i18n: DEBUG
|
||||||
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE # \u6253\u5370\u6240\u6709\u6CE8\u518C\u7684\u63A5\u53E3\u8DEF\u5F84
|
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
|
||||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE # \u663E\u793ASQL\u53C2\u6570
|
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||||
org.hibernate.type.descriptor.sql: TRACE
|
org.hibernate.type.descriptor.sql: TRACE
|
||||||
com.qqchen.deploy.backend.common.utils.EntityPathResolver: DEBUG
|
com.qqchen.deploy.backend.common.utils.EntityPathResolver: DEBUG
|
||||||
jwt:
|
jwt:
|
||||||
|
|||||||
@ -7,6 +7,9 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE
|
|||||||
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
||||||
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
||||||
|
|
||||||
|
|
||||||
|
tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
|
||||||
|
|
||||||
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
||||||
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
||||||
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
||||||
@ -7,6 +7,9 @@ response.forbidden=Forbidden
|
|||||||
response.not.found=Resource Not Found
|
response.not.found=Resource Not Found
|
||||||
response.conflict=Resource Conflict
|
response.conflict=Resource Conflict
|
||||||
|
|
||||||
|
|
||||||
|
tenant.not.found=Tenant not found
|
||||||
|
|
||||||
user.not.found=User not found
|
user.not.found=User not found
|
||||||
user.username.exists=Username already exists
|
user.username.exists=Username already exists
|
||||||
user.email.exists=Email already exists
|
user.email.exists=Email already exists
|
||||||
@ -7,6 +7,8 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE
|
|||||||
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
response.not.found=\u8D44\u6E90\u672A\u627E\u5230
|
||||||
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
response.conflict=\u8D44\u6E90\u51B2\u7A81
|
||||||
|
|
||||||
|
tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728
|
||||||
|
|
||||||
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
user.not.found=\u7528\u6237\u4E0D\u5B58\u5728
|
||||||
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728
|
||||||
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728
|
||||||
Loading…
Reference in New Issue
Block a user