diff --git a/backend/pom.xml b/backend/pom.xml index 8274d359..4f433a24 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -24,6 +24,8 @@ 1.18.30 6.2.0 0.12.3 + 2.2.0 + 5.8.23 @@ -140,6 +142,18 @@ ${jjwt.version} runtime + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + cn.hutool + hutool-all + ${hutool.version} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java index 34a9ca91..bb6f8c2d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/api/UserApiController.java @@ -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.enums.ResponseCode; 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.dto.query.UserQuery; import com.qqchen.deploy.backend.dto.request.UserRequest; import com.qqchen.deploy.backend.dto.response.UserResponse; import com.qqchen.deploy.backend.dto.request.UserRegisterRequest; import com.qqchen.deploy.backend.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.data.domain.Page; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -29,13 +32,18 @@ public class UserApiController extends BaseController login(@Validated @RequestBody LoginRequest request) { + return Response.success(userService.login(request)); + } + @PostMapping("/register") public Response register(@Validated @RequestBody UserRegisterRequest request) { // 基础的注册逻辑 if (!request.getPassword().equals(request.getConfirmPassword())) { return Response.error(ResponseCode.USER_NOT_FOUND); } - User user = converter.toEntity(request); User savedUser = userService.register(user); return Response.success(converter.toResponse(savedUser)); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java new file mode 100644 index 00000000..f725e0e7 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/TenantFilter.java @@ -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; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java new file mode 100644 index 00000000..ea732f76 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/config/JacksonConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java b/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java new file mode 100644 index 00000000..9dd0d62c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/context/TenantContext.java @@ -0,0 +1,22 @@ +package com.qqchen.deploy.backend.common.context; + +public class TenantContext { + private static final ThreadLocal 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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java index 1049a7e1..66220825 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/enums/ResponseCode.java @@ -13,9 +13,10 @@ public enum ResponseCode { CONFLICT(409, "response.conflict"), // 业务错误码 - USER_NOT_FOUND(1001, "user.not.found"), - USERNAME_EXISTS(1002, "user.username.exists"), - EMAIL_EXISTS(1003, "user.email.exists"); + TENANT_NOT_FOUND(1001, "tenant.not.found"), + USER_NOT_FOUND(2001, "user.not.found"), + USERNAME_EXISTS(2002, "user.username.exists"), + EMAIL_EXISTS(2003, "user.email.exists"); private final int code; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java b/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java index 86cc5318..843fb688 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/event/DomainEvent.java @@ -6,9 +6,11 @@ import java.time.LocalDateTime; @Getter public abstract class DomainEvent { + private final String eventId; + private final LocalDateTime occurredOn; - + protected DomainEvent() { this.eventId = java.util.UUID.randomUUID().toString(); this.occurredOn = LocalDateTime.now(); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java new file mode 100644 index 00000000..a1621bb1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/BusinessException.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..e0d6b388 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/exception/GlobalExceptionHandler.java @@ -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 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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java new file mode 100644 index 00000000..6bc77fe6 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/filter/TenantFilter.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java b/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java new file mode 100644 index 00000000..37daf740 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/interceptor/TenantInterceptor.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java index a615db86..bd6661a2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/config/SecurityConfig.java @@ -1,5 +1,8 @@ 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.Configuration; 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.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -22,8 +26,13 @@ import java.util.Arrays; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + private final CustomAuthenticationEntryPoint authenticationEntryPoint; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -31,10 +40,14 @@ public class SecurityConfig { .csrf(csrf -> csrf.disable()) // 禁用CSRF .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(authenticationEntryPoint) + ) .formLogin(form -> form.disable()) // 禁用form登录 .httpBasic(basic -> basic.disable()) // 禁用basic认证 .logout(logout -> logout.disable()) // 禁用logout - .anonymous(anonymous -> {}) // 允许匿名访问 + .anonymous(anonymous -> { + }) // 允许匿名访问 .authorizeHttpRequests(auth -> auth // 公开接口 // .requestMatchers("/api/v1/users/register").permitAll() @@ -47,7 +60,8 @@ public class SecurityConfig { .anyRequest().permitAll() // 生产环境建议改为需要认证 //.anyRequest().authenticated() - ); + + ).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java index 20e29325..35f79b4d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/filter/JwtAuthenticationFilter.java @@ -16,6 +16,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.Arrays; +import java.util.List; @Slf4j @Component @@ -23,13 +25,25 @@ import java.io.IOException; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; - private final UserDetailsService userDetailsService; + // 添加白名单路径 + private static final List 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 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - try { String authHeader = request.getHeader("Authorization"); String token = null; @@ -39,10 +53,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { token = authHeader.substring(7); username = jwtTokenUtil.getUsernameFromToken(token); } - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); - if (jwtTokenUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -50,10 +62,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { SecurityContextHolder.getContext().setAuthentication(authentication); } } + chain.doFilter(request, response); } catch (Exception e) { log.error("Cannot set user authentication", e); + chain.doFilter(request, response); } - - chain.doFilter(request, response); } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java b/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..91505161 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/security/handler/CustomAuthenticationEntryPoint.java @@ -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)); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java new file mode 100644 index 00000000..c095c577 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/dto/request/LoginRequest.java @@ -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; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java new file mode 100644 index 00000000..8aa5937d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/dto/response/LoginResponse.java @@ -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; + +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java index f0b8b85f..de608ab9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IUserService.java @@ -1,6 +1,8 @@ package com.qqchen.deploy.backend.service; 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; public interface IUserService extends IBaseService { @@ -8,4 +10,6 @@ public interface IUserService extends IBaseService { User findByUsername(String username); boolean checkUsernameExists(String username); boolean checkEmailExists(String email); + + LoginResponse login(LoginRequest request); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java new file mode 100644 index 00000000..7896be87 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/CustomUserDetailsService.java @@ -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(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java index 3d679878..88edd9e1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/UserServiceImpl.java @@ -1,19 +1,35 @@ 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.dto.request.LoginRequest; +import com.qqchen.deploy.backend.dto.response.LoginResponse; import com.qqchen.deploy.backend.entity.User; import com.qqchen.deploy.backend.repository.IUserRepository; 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.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; @Service -@Transactional +@Slf4j public class UserServiceImpl extends BaseServiceImpl implements IUserService { private final IUserRepository userRepository; + @Resource + private AuthenticationManager authenticationManager; + + @Resource + private JwtTokenUtil jwtTokenUtil; + @Autowired public UserServiceImpl(IUserRepository userRepository) { super(userRepository); @@ -47,4 +63,24 @@ public class UserServiceImpl extends BaseServiceImpl implements IUse 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; + } + } \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 7df400bd..ffd9f797 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: jdbc: time_zone: Asia/Shanghai mvc: - log-request-details: true # \u6253\u5370\u8BF7\u6C42\u8BE6\u60C5 + log-request-details: true messages: basename: messages encoding: UTF-8 @@ -35,8 +35,8 @@ logging: level: org.springframework.web: 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.hibernate.type.descriptor.sql.BasicBinder: TRACE # \u663E\u793ASQL\u53C2\u6570 + org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE + org.hibernate.type.descriptor.sql.BasicBinder: TRACE org.hibernate.type.descriptor.sql: TRACE com.qqchen.deploy.backend.common.utils.EntityPathResolver: DEBUG jwt: diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 251a9a10..51c98249 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -7,6 +7,9 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE response.not.found=\u8D44\u6E90\u672A\u627E\u5230 response.conflict=\u8D44\u6E90\u51B2\u7A81 + +tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728 + user.not.found=\u7528\u6237\u4E0D\u5B58\u5728 user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728 user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728 \ No newline at end of file diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 899ab0e9..5ded67ea 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -7,6 +7,9 @@ response.forbidden=Forbidden response.not.found=Resource Not Found response.conflict=Resource Conflict + +tenant.not.found=Tenant not found + user.not.found=User not found user.username.exists=Username already exists user.email.exists=Email already exists \ No newline at end of file diff --git a/backend/src/main/resources/messages_zh_CN.properties b/backend/src/main/resources/messages_zh_CN.properties index 251a9a10..fb120b64 100644 --- a/backend/src/main/resources/messages_zh_CN.properties +++ b/backend/src/main/resources/messages_zh_CN.properties @@ -7,6 +7,8 @@ response.forbidden=\u7981\u6B62\u8BBF\u95EE response.not.found=\u8D44\u6E90\u672A\u627E\u5230 response.conflict=\u8D44\u6E90\u51B2\u7A81 +tenant.not.found=\u79DF\u6237\u4E0D\u5B58\u5728 + user.not.found=\u7528\u6237\u4E0D\u5B58\u5728 user.username.exists=\u7528\u6237\u540D\u5DF2\u5B58\u5728 user.email.exists=\u90AE\u7BB1\u5DF2\u5B58\u5728 \ No newline at end of file