From 9b28ef0a37b5ce08b641b0ca450f74778e96605f Mon Sep 17 00:00:00 2001 From: meowrain Date: Wed, 19 Nov 2025 23:12:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5spring=20security?= =?UTF-8?q?=E5=92=8Coauth2=E5=BA=93=E8=BF=98=E6=9C=89jwt=E5=BA=93=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E6=9C=AC=E7=9A=84=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=92=8C=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 1 - aioj-backend-user-service/pom.xml | 30 ++++++++ .../constants/UserServiceChainConstants.java | 5 -- ...hianMarkEnums.java => ChainMarkEnums.java} | 8 +- .../config/FrameworkConfiguration.java | 3 + .../config/JwtPropertiesConfiguration.java | 21 +++++ .../config/SecurityConfiguration.java | 56 ++++++++++++++ .../controller/UserController.java | 31 +++++++- .../UserLoginRequestParamVerifyChain.java | 37 +++++++++ .../UserRegisterRequestParamVerifyChain.java | 4 +- .../UserLoginRequestParamVerifyContext.java | 9 +++ .../userservice/dto/req/UserLoginRequest.java | 3 - .../dto/req/UserRegisterRequest.java | 3 +- .../dto/resp/UserLoginResponse.java | 32 +++++++- .../security/CustomUserDetailsService.java | 35 +++++++++ .../security/JwtAuthenticationFilter.java | 76 +++++++++++++++++++ .../userservice/service/UserService.java | 11 ++- .../service/impl/UserServiceImpl.java | 54 +++++++++++-- .../backend/userservice/utils/JwtUtil.java | 59 ++++++++++++++ .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application-prod.yml | 2 +- .../src/main/resources/application.yml | 5 +- pom.xml | 43 ++++++++++- 23 files changed, 496 insertions(+), 34 deletions(-) delete mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/constants/UserServiceChainConstants.java rename aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/{ChianMarkEnums.java => ChainMarkEnums.java} (72%) create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/JwtPropertiesConfiguration.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/SecurityConfiguration.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserLoginRequestParamVerifyChain.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/context/UserLoginRequestParamVerifyContext.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/CustomUserDetailsService.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/JwtAuthenticationFilter.java create mode 100644 aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/utils/JwtUtil.java diff --git a/.idea/misc.xml b/.idea/misc.xml index e2516d5..15a25de 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,6 @@ - \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/constants/UserServiceChainConstants.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/constants/UserServiceChainConstants.java deleted file mode 100644 index 12ac2c9..0000000 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/constants/UserServiceChainConstants.java +++ /dev/null @@ -1,5 +0,0 @@ -package cn.meowrain.aioj.backend.userservice.common.constants; - -public class UserServiceChainConstants { - protected static final String register_req_verify = "register_req_verify"; -} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChianMarkEnums.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChainMarkEnums.java similarity index 72% rename from aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChianMarkEnums.java rename to aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChainMarkEnums.java index de3f64c..d62fe62 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChianMarkEnums.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/common/enums/ChainMarkEnums.java @@ -4,11 +4,15 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public enum ChianMarkEnums { +public enum ChainMarkEnums { /** * 用户注册请求验证 */ - USER_REGISTER_REQ_PARAM_VERIFY("USER_REGISTER_REQ_PARAM_VERIFY"); + USER_REGISTER_REQ_PARAM_VERIFY("USER_REGISTER_REQ_PARAM_VERIFY"), + /** + * 用户登录请求验证 + */ + USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY"); @Getter private final String markName; diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/FrameworkConfiguration.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/FrameworkConfiguration.java index 6af99f6..0cb15b2 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/FrameworkConfiguration.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/FrameworkConfiguration.java @@ -1,5 +1,6 @@ package cn.meowrain.aioj.backend.userservice.config; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScans; import org.springframework.context.annotation.Configuration; @@ -9,4 +10,6 @@ import org.springframework.context.annotation.Configuration; @ComponentScan("cn.meowrain.aioj.backend.framework.banner") }) public class FrameworkConfiguration { + + } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/JwtPropertiesConfiguration.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/JwtPropertiesConfiguration.java new file mode 100644 index 0000000..e744365 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/JwtPropertiesConfiguration.java @@ -0,0 +1,21 @@ +package cn.meowrain.aioj.backend.userservice.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Data +@ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX) +public class JwtPropertiesConfiguration { + public static final String PREFIX = "jwt"; + /** + * JWT 密钥(必须 32 字节以上) + */ + private String secret; + + /** + * 过期时间(单位:毫秒) + */ + private Long expire; +} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/SecurityConfiguration.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/SecurityConfiguration.java new file mode 100644 index 0000000..e51c723 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/config/SecurityConfiguration.java @@ -0,0 +1,56 @@ +package cn.meowrain.aioj.backend.userservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import cn.meowrain.aioj.backend.userservice.security.JwtAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfiguration(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/auth/**", + "/doc.html", + "/swagger-ui/**", + "/swagger-resources/**", + "/webjars/**", + "/v3/api-docs/**", + "/favicon.ico" + ) + .permitAll() + .anyRequest().authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } +} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java index c51f2fe..7c8c633 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java @@ -1,7 +1,36 @@ package cn.meowrain.aioj.backend.userservice.controller; +import cn.meowrain.aioj.backend.framework.web.Result; +import cn.meowrain.aioj.backend.framework.web.Results; +import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; +import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; +import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse; +import cn.meowrain.aioj.backend.userservice.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@RestController("/user") +@RequiredArgsConstructor +@RestController() +@RequestMapping("/auth") public class UserController { + + private final UserService userService; + + + @PostMapping("/register") + public Result register(@RequestBody UserRegisterRequest userRegisterRequest) { + Long l = userService.userRegister(userRegisterRequest); + return Results.success(l); + } + + @PostMapping("/login") + public Result login(@RequestBody UserLoginRequest userLoginRequest) { + UserLoginResponse userLoginResponse = userService.userLogin(userLoginRequest); + return Results.success(userLoginResponse); + + } + } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserLoginRequestParamVerifyChain.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserLoginRequestParamVerifyChain.java new file mode 100644 index 0000000..d8b7343 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserLoginRequestParamVerifyChain.java @@ -0,0 +1,37 @@ +package cn.meowrain.aioj.backend.userservice.dto.chains; + +import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler; +import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; +import cn.meowrain.aioj.backend.framework.exception.ClientException; +import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; +import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class UserLoginRequestParamVerifyChain implements AbstractChianHandler { + @Override + public void handle(UserLoginRequest requestParam) { + if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) { + throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR); + } + if (requestParam.getUserAccount().length() < 4) { + throw new ClientException("账号长度不小于4位", ErrorCode.PARAMS_ERROR); + } + if (requestParam.getUserPassword().length() < 8) { + throw new ClientException("密码长度不小于8位", ErrorCode.PARAMS_ERROR); + } + } + + @Override + public String mark() { + return ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(); + } + + @Override + public int getOrder() { + return 10; + } +} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java index 192baae..f8f16b4 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java @@ -3,7 +3,7 @@ package cn.meowrain.aioj.backend.userservice.dto.chains; import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler; import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.exception.ClientException; -import cn.meowrain.aioj.backend.userservice.common.enums.ChianMarkEnums; +import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import com.alibaba.nacos.common.utils.StringUtils; import lombok.extern.slf4j.Slf4j; @@ -33,7 +33,7 @@ public class UserRegisterRequestParamVerifyChain implements AbstractChianHandler @Override public String mark() { - return ChianMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(); + return ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(); } @Override diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/context/UserLoginRequestParamVerifyContext.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/context/UserLoginRequestParamVerifyContext.java new file mode 100644 index 0000000..d4ea053 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/context/UserLoginRequestParamVerifyContext.java @@ -0,0 +1,9 @@ +package cn.meowrain.aioj.backend.userservice.dto.chains.context; + +import cn.meowrain.aioj.backend.framework.designpattern.chains.CommonChainContext; +import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; +import org.springframework.stereotype.Component; + +@Component +public class UserLoginRequestParamVerifyContext extends CommonChainContext { +} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserLoginRequest.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserLoginRequest.java index a92d012..372f5a5 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserLoginRequest.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserLoginRequest.java @@ -1,13 +1,10 @@ package cn.meowrain.aioj.backend.userservice.dto.req; import jakarta.servlet.http.HttpServletRequest; -import lombok.Builder; import lombok.Data; @Data -@Builder public class UserLoginRequest { private String userAccount; private String userPassword; - HttpServletRequest request; } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserRegisterRequest.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserRegisterRequest.java index c419f7a..32239d0 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserRegisterRequest.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/UserRegisterRequest.java @@ -1,10 +1,9 @@ package cn.meowrain.aioj.backend.userservice.dto.req; -import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data -@Builder public class UserRegisterRequest { private String userAccount; private String userPassword; diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponse.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponse.java index da58620..b8ca516 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponse.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponse.java @@ -1,5 +1,6 @@ package cn.meowrain.aioj.backend.userservice.dto.resp; +import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.io.Serializable; @@ -7,11 +8,26 @@ import java.util.Date; @Data public class UserLoginResponse implements Serializable { + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; /** - * 用户 id + * 用户账号 */ - private Long id; + private String userAccount; + + /** + * 开放平台id + */ + private String unionId; + + /** + * 公众号openId + */ + private String mpOpenId; /** * 用户昵称 @@ -43,5 +59,17 @@ public class UserLoginResponse implements Serializable { */ private Date updateTime; + /** + * 是否删除 + */ + + private Integer isDelete; + + + /** + * JWT令牌(登录成功返回) + */ + private String token; + private static final long serialVersionUID = 1L; } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/CustomUserDetailsService.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/CustomUserDetailsService.java new file mode 100644 index 0000000..d3e8827 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/CustomUserDetailsService.java @@ -0,0 +1,35 @@ +package cn.meowrain.aioj.backend.userservice.security; + +import cn.meowrain.aioj.backend.userservice.dao.entity.User; +import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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 java.util.Collection; +import java.util.Collections; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserMapper userMapper; + + public CustomUserDetailsService(UserMapper userMapper) { + this.userMapper = userMapper; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class).eq(User::getUserAccount, username)); + if (user == null) { + throw new UsernameNotFoundException("用户不存在: " + username); + } + Collection authorities = Collections + .singletonList(new SimpleGrantedAuthority(user.getUserRole())); + return new org.springframework.security.core.userdetails.User(user.getUserAccount(), user.getUserPassword(), + authorities); + } +} \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/JwtAuthenticationFilter.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..7f676cd --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/security/JwtAuthenticationFilter.java @@ -0,0 +1,76 @@ +package cn.meowrain.aioj.backend.userservice.security; + +import cn.meowrain.aioj.backend.userservice.dao.entity.User; +import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper; +import cn.meowrain.aioj.backend.userservice.utils.JwtUtil; +import io.jsonwebtoken.Claims; +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.Collections; +import java.util.List; + +/** + * JWT请求过滤器:解析 Authorization 头中的 Bearer token 并设置认证上下文 + */ +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final UserMapper userMapper; + + public JwtAuthenticationFilter(JwtUtil jwtUtil, UserMapper userMapper) { + this.jwtUtil = jwtUtil; + this.userMapper = userMapper; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String header = request.getHeader("Authorization"); + if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + String token = header.substring(7); + if (!jwtUtil.isTokenValid(token)) { + filterChain.doFilter(request, response); + return; + } + try { + Claims claims = jwtUtil.parseClaims(token); + String account = claims.getSubject(); + Long userId = claims.get("userId", Long.class); + + // 避免重复设置 + if (StringUtils.hasText(account) && SecurityContextHolder.getContext().getAuthentication() == null) { + // 可选:从数据库再查一次,保证用户未被删除/封禁 + User user = userMapper.selectById(userId); + if (user == null || !StringUtils.hasText(user.getUserRole())) { + filterChain.doFilter(request, response); + return; + } + List authorities = Collections + .singletonList(new SimpleGrantedAuthority(user.getUserRole())); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(account, + null, authorities); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception ignored) { + // 解析失败直接继续,不抛出,保持无状态 + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java index 07b50a1..1e29a9e 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java @@ -1,16 +1,23 @@ package cn.meowrain.aioj.backend.userservice.service; import cn.meowrain.aioj.backend.userservice.dao.entity.User; +import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; +import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse; import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IService { /** * 用户注册 * @param request {@link UserRegisterRequest} - * @return + * @return {@link Long} */ Long userRegister(UserRegisterRequest request); - + /** + * 用户登录 + * @param request {@link UserLoginRequest} + * @return {@link UserLoginResponse} + */ + UserLoginResponse userLogin(UserLoginRequest request); } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java index abca8d9..a401139 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java @@ -1,33 +1,48 @@ package cn.meowrain.aioj.backend.userservice.service.impl; +import cn.hutool.core.util.ObjectUtil; import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.exception.ServiceException; -import cn.meowrain.aioj.backend.userservice.common.enums.ChianMarkEnums; +import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.dao.entity.User; import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper; +import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserLoginRequestParamVerifyContext; import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserRegisterRequestParamVerifyContext; +import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; +import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse; import cn.meowrain.aioj.backend.userservice.service.UserService; +import cn.meowrain.aioj.backend.userservice.utils.JwtUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import org.springframework.util.DigestUtils; + +import java.util.Date; @Service @RequiredArgsConstructor public class UserServiceImpl extends ServiceImpl implements UserService { private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext; - private final String SALT = "meowrain"; + private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; @Override public Long userRegister(UserRegisterRequest request) { - userRegisterRequestParamVerifyContext.handler(ChianMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(), request); - // 加密 - String encryptPassword = DigestUtils.md5DigestAsHex((SALT + request.getUserPassword()).getBytes()); - User user = new User().setUserAccount(request.getUserAccount()).setUserPassword(encryptPassword); + userRegisterRequestParamVerifyContext.handler(ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(), + request); + // 使用 BCrypt 加密密码 + Date now = new Date(); + String encryptPassword = passwordEncoder.encode(request.getUserPassword()); + User user = new User().setUserAccount(request.getUserAccount()).setUserPassword(encryptPassword) + .setUserRole("user").setCreateTime(now).setUpdateTime(now); + try { + // 需要修改表,使得用户名是唯一的 this.save(user); } catch (DuplicateKeyException e) { log.error("重复创建用户"); @@ -36,4 +51,29 @@ public class UserServiceImpl extends ServiceImpl implements Us return user.getId(); } + + @Override + public UserLoginResponse userLogin(UserLoginRequest requestParam) { + // 1.校验 + userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(), + requestParam); + User user = this.baseMapper.selectOne(Wrappers.lambdaQuery(User.class) + .eq(User::getUserAccount, requestParam.getUserAccount())); + if (ObjectUtil.isNull(user) + || !passwordEncoder.matches(requestParam.getUserPassword(), user.getUserPassword())) { + throw new ServiceException("用户不存在或者密码错误", ErrorCode.NOT_LOGIN_ERROR); + } + // 生成 JWT + String token = jwtUtil.generateToken(user); + UserLoginResponse resp = new UserLoginResponse(); + resp.setId(user.getId()); + resp.setUserAccount(user.getUserAccount()); + resp.setUserAvatar(user.getUserAvatar()); + resp.setUserProfile(user.getUserProfile()); + resp.setUserRole(user.getUserRole()); + resp.setCreateTime(user.getCreateTime()); + resp.setUpdateTime(user.getUpdateTime()); + resp.setToken(token); + return resp; + } } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/utils/JwtUtil.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/utils/JwtUtil.java new file mode 100644 index 0000000..e4702c8 --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/utils/JwtUtil.java @@ -0,0 +1,59 @@ +package cn.meowrain.aioj.backend.userservice.utils; + +import cn.meowrain.aioj.backend.userservice.config.JwtPropertiesConfiguration; +import cn.meowrain.aioj.backend.userservice.dao.entity.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT工具类 + */ +@RequiredArgsConstructor +@Component +public class JwtUtil { + private final JwtPropertiesConfiguration jwtConfig; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes()); + } + + public String generateToken(User user) { + long now = System.currentTimeMillis(); + Map claims = new HashMap<>(); + claims.put("userId", user.getId()); + claims.put("role", user.getUserRole()); + return Jwts.builder() + .subject(user.getUserAccount()) + .issuedAt(new Date(now)) + .expiration(new Date(now + jwtConfig.getExpire())) + .claims(claims) + .signWith(getSigningKey(), Jwts.SIG.HS256) + .compact(); + } + + public Claims parseClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public boolean isTokenValid(String token) { + try { + Claims claims = parseClaims(token); + Date expiration = claims.getExpiration(); + return expiration != null && expiration.after(new Date()); + } catch (Exception e) { + return false; + } + } +} diff --git a/aioj-backend-user-service/src/main/resources/application-dev.yml b/aioj-backend-user-service/src/main/resources/application-dev.yml index 3c2f2a4..4769411 100644 --- a/aioj-backend-user-service/src/main/resources/application-dev.yml +++ b/aioj-backend-user-service/src/main/resources/application-dev.yml @@ -8,7 +8,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.0.0.10/aioj_dev username: root - password: 123456 + password: root cloud: nacos: discovery: diff --git a/aioj-backend-user-service/src/main/resources/application-prod.yml b/aioj-backend-user-service/src/main/resources/application-prod.yml index 07caeb5..ed56224 100644 --- a/aioj-backend-user-service/src/main/resources/application-prod.yml +++ b/aioj-backend-user-service/src/main/resources/application-prod.yml @@ -8,4 +8,4 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.0.0.10/aioj_prod username: root - password: 123456 \ No newline at end of file + password: root \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/resources/application.yml b/aioj-backend-user-service/src/main/resources/application.yml index f6ac7e7..d8873e5 100644 --- a/aioj-backend-user-service/src/main/resources/application.yml +++ b/aioj-backend-user-service/src/main/resources/application.yml @@ -17,7 +17,7 @@ springdoc: group-configs: - group: 'default' paths-to-match: '/**' - packages-to-scan: cn.meowrain.aioj + packages-to-scan: cn.meowrain.aioj.backend.userservice.controller knife4j: basic: enable: true @@ -27,3 +27,6 @@ mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:/mapper/**/*.xml +jwt: + secret: "12345678901234567890123456789012" # 至少32字节!! + expire: 86400000 # 24小时(单位:毫秒) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3d2b2ae..cdbceba 100644 --- a/pom.xml +++ b/pom.xml @@ -44,10 +44,17 @@ com.alibaba.cloud spring-cloud-starter-alibaba-sentinel - - - - + + + org.springframework.boot + spring-boot-starter-cache + + + + + + + com.baomidou mybatis-plus-spring-boot3-starter @@ -128,6 +135,34 @@ knife4j-openapi3-jakarta-spring-boot-starter 4.5.0 + + + org.springframework.boot + spring-boot-starter-security + 3.5.7 + + + + + + + org.springframework.boot + spring-boot-starter-oauth2-client + 3.5.7 + + + + org.springframework.security + spring-security-test + 6.5.7 + test + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + 3.5.7 +