From 2e4128ab406d49c2721e3f00804ddf62591d6776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E6=AC=A7=EF=BC=88=E6=9E=97=E5=B9=B3=E5=87=A1?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 16 Jun 2025 16:36:25 +0800 Subject: [PATCH] =?UTF-8?q?=E9=89=B4=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.cache/.Apifox_Helper/.toolWindow.db | Bin 122880 -> 122880 bytes .idea/ApifoxUploaderProjectSetting.xml | 9 ++ pom.xml | 51 ++++++-- .../cn/van/business/config/KaptchaConfig.java | 27 ++++ .../van/business/config/SecurityConfig.java | 74 +++++++++++ .../business/controller/BaseController.java | 67 ++++++++++ .../controller/jd/OrderController.java | 22 +--- .../controller/login/AuthController.java | 120 ++++++++++++++++++ .../controller/login/CaptchaController.java | 24 ++++ .../cn/van/business/filter/JwtAuthFilter.java | 62 +++++++++ .../cn/van/business/filter/RequireAuth.java | 13 ++ .../cn/van/business/model/user/AdminUser.java | 46 +++++++ .../van/business/model/user/LoginRequest.java | 13 ++ .../business/model/user/LoginResponse.java | 14 ++ .../cn/van/business/model/user/UserInfo.java | 16 +++ .../business/service/AdminUserService.java | 29 +++++ .../van/business/service/CaptchaService.java | 32 +++++ .../service/impl/UserDetailsServiceImpl.java | 36 ++++++ .../cn/van/business/util/AuthEntryPoint.java | 18 +++ .../java/cn/van/business/util/JDUtil.java | 3 + .../java/cn/van/business/util/JwtUtils.java | 107 ++++++++++++++++ src/main/resources/application-dev.yml | 12 +- 22 files changed, 757 insertions(+), 38 deletions(-) create mode 100644 src/main/java/cn/van/business/config/KaptchaConfig.java create mode 100644 src/main/java/cn/van/business/config/SecurityConfig.java create mode 100644 src/main/java/cn/van/business/controller/BaseController.java create mode 100644 src/main/java/cn/van/business/controller/login/AuthController.java create mode 100644 src/main/java/cn/van/business/controller/login/CaptchaController.java create mode 100644 src/main/java/cn/van/business/filter/JwtAuthFilter.java create mode 100644 src/main/java/cn/van/business/filter/RequireAuth.java create mode 100644 src/main/java/cn/van/business/model/user/AdminUser.java create mode 100644 src/main/java/cn/van/business/model/user/LoginRequest.java create mode 100644 src/main/java/cn/van/business/model/user/LoginResponse.java create mode 100644 src/main/java/cn/van/business/model/user/UserInfo.java create mode 100644 src/main/java/cn/van/business/service/AdminUserService.java create mode 100644 src/main/java/cn/van/business/service/CaptchaService.java create mode 100644 src/main/java/cn/van/business/service/impl/UserDetailsServiceImpl.java create mode 100644 src/main/java/cn/van/business/util/AuthEntryPoint.java create mode 100644 src/main/java/cn/van/business/util/JwtUtils.java diff --git a/.idea/.cache/.Apifox_Helper/.toolWindow.db b/.idea/.cache/.Apifox_Helper/.toolWindow.db index d585eb6cb8a34ea5d90a32743211eb31498d8d83..c9a9adbd2389e47e356050f8bab23d12f7ca2b62 100644 GIT binary patch delta 3129 zcmcguOK4nG7|u;n^XeFD8cMC13DbxMW9Hm*&pnT%$(W|L6gqv`M^v<)*KLzdX5!4G zN!5}zHeh^!^)6}`LIn|YA%-prE~E=}A&3h>1iNvsATG3^3;#PwCy7Zr>cR{2IA{L( z&;R(o|2yYzG@QTD@YXgbe(AwsC%*9f`-yn0=}JxFlWjc@G;iIynYEs|cW3O-M%6Oc z{8@8d)0KrwQ%$v}Vxgg9&c^T?`l-_rzQ(?En!`ikqacUI`JWWYpnUqQ~g&5Y_C&XYAU=up5J$#3Egl~(s$kFS47iPvx z?}37`h2eazRLEzuw$M8!&&X=w%@5wwo|$XSoM=mJotw(cJ-n&4oEA5hdA=J66cB-o zPcd;lpBse~C(Jb&TRN3DL;8iu(Sm(MXTw+fw@0CT)ZdZJjuuNjoqDoRu({IFLUtfk zusUBb<@i$3uiC`~jo9>RB*d4>(+I+gX)m#Z`5tSDE zo>t8IT5*A?Wj?~@@+IMHp4qu)dO~}L26m*>*^+%s77Ft0;LyH(Yl0Xrr|?oTmp68= zN%keH<(%wEMq!q1J=e1$Fw!cG1Li7i0%4R4tZ)_4L|ZUkJYc^ZUv%#x34hVd5Z_bC z7$5rvS?H&QS_|ri*9L}0ndB667a__5C8UO21ELKkMl;!aEvIJ2BTSf5JHCJcJ-x(A<@ORF_VgFE#PxK&?L`Q0iX)yV7XKCgPjjW z4TZCo`wge9DR%h7;Bd=|j)z@oFKYF%^zH*|Tb(>NX59G`8vvTyGx;`AhGzc~G+nN= z@MsVyeKS1?LR)U4;X55 z;d_0Zt;1RK)R8^e7tNkylTVNA4ZrUrGgH&Ow{32#MwP(lo!a)yMVUF?wtef|c;@2S zC2DHW3TinL21c@rT;dTT5j8G|h(%cZT{UE}xL<~E_Q%Dl7?!-DsZ~DXwMZ@-b_u zSS{6+R8$iCl35tdNV`5&)aQYaFzO*LW~2h!@!ql;Rm6e;?K~G@W}wMF$OI+WBSO}S z8Y81>0@x54Rl6|U6B(_T*)!|v)rN9VRTx(v8C7dcU1W3@U~Vasu(rOgwFyPH*V>LN zHSw=H;PxBUaG<$=wikHamU`s?+-}?91t98 ztXl_gvg+DUna7u%FPtC4KW7>nYZKRN5;qgq7XyA~xVzi!R)BVN6^|?ADFA(ZslL<%&eseiD~NT{xX465Mc;o4nlIckVssp4IK9)$OJy z-5|7iJOx74tIz0wuU&J|ue!&&x|XdoMXJHShD^1E1ydHl-Sd1{5Cdaf%JMY(E$bc~?E3kd5B)>W0&F~m@1Bwp7f zBuHdzaDu$=zeUCe0_3Y4>D*Xw2bFb7x#<@|_lK1A6K$;yP!x$p&84-R;rLM zT7_H=Tgk`*E@leGw7P(YEo@qNDRUvaR4iD_$%uZ#vT(k5*~$%vl9>Fz}=i9gI30=Ay~yqFT&26&_>63Jixi%`{m~WK6+e zOrFK24xu5icJDqr=ZB>6h_!`-vcTz*$ck9wu*qP~#Jr)3B3T^HjkBDrV?k!2EMjEi zvK(ZgsOc&%X-E`s^zNZ|d4qb%H$;aIDr)buFKLaDBkGrtJGQ7n@{@zq0a3||(G<8u zevEbmeBnJ8{X4uzf2JSPRl0{Br|0X|deTBh=i&{%xJLmbl!(|4do&lf9kyjx8=R_b zjD)xLI&5uEt;v!3sN3slZH6@$g7B^@yjvgkpry0a1A>ZQxOdX_XwXhO{y8jGBXr6= ze~vIKQaQo^{@7%8DwWRY`E&-)D%qLbQnvgu24wPQ#n%Yv9T&Y*M=l-l#ym>1GrUxJ IUIAl&0c6Dp+yDRo diff --git a/.idea/ApifoxUploaderProjectSetting.xml b/.idea/ApifoxUploaderProjectSetting.xml index b2af8c3..a839c2e 100644 --- a/.idea/ApifoxUploaderProjectSetting.xml +++ b/.idea/ApifoxUploaderProjectSetting.xml @@ -1,6 +1,15 @@ + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2418b66..682980e 100644 --- a/pom.xml +++ b/pom.xml @@ -90,18 +90,39 @@ jdk 3.0 + + com.github.penggle + kaptcha + 2.3.2 + + + org.codehaus.jackson + jackson-mapper-asl + 1.9.2 + - - org.codehaus.jackson - jackson-mapper-asl - 1.9.2 - - - - org.codehaus.jackson - jackson-core-asl - 1.9.2 - + + org.codehaus.jackson + jackson-core-asl + 1.9.2 + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + @@ -119,6 +140,10 @@ httpclient 4.5.13 + + org.springframework.boot + spring-boot-starter-security + @@ -148,13 +173,13 @@ maven-local88 Local Repository - http://192.168.8.88:8081/repository/maven-local88/ + http://134.175.126.60:38081/repository/maven-local88/ true always - false + true diff --git a/src/main/java/cn/van/business/config/KaptchaConfig.java b/src/main/java/cn/van/business/config/KaptchaConfig.java new file mode 100644 index 0000000..9cab3e1 --- /dev/null +++ b/src/main/java/cn/van/business/config/KaptchaConfig.java @@ -0,0 +1,27 @@ +package cn.van.business.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +@Configuration +public class KaptchaConfig { + + @Bean + public DefaultKaptcha kaptchaProducer() { + DefaultKaptcha kaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + + // 设置验证码参数 + properties.setProperty("kaptcha.image.width", "150"); + properties.setProperty("kaptcha.image.height", "50"); + properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + properties.setProperty("kaptcha.textproducer.char.length", "4"); + + kaptcha.setConfig(new Config(properties)); + return kaptcha; + } +} diff --git a/src/main/java/cn/van/business/config/SecurityConfig.java b/src/main/java/cn/van/business/config/SecurityConfig.java new file mode 100644 index 0000000..b0e721a --- /dev/null +++ b/src/main/java/cn/van/business/config/SecurityConfig.java @@ -0,0 +1,74 @@ +package cn.van.business.config; + +import cn.van.business.filter.JwtAuthFilter; +import cn.van.business.service.impl.UserDetailsServiceImpl; +import cn.van.business.util.JwtUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; +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 org.springframework.security.web.session.HttpSessionEventPublisher; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final UserDetailsServiceImpl userDetailsService; + private final JwtAuthFilter jwtAuthFilter; + + public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthFilter jwtAuthFilter) { + this.userDetailsService = userDetailsService; + this.jwtAuthFilter = jwtAuthFilter; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, + PasswordEncoder passwordEncoder) throws Exception { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder); + + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .authenticationProvider(provider) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/login", "/captcha").permitAll() + .anyRequest().authenticated()); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public SessionRegistry sessionRegistry() { + return new SessionRegistryImpl(); + } + + @Bean + public HttpSessionEventPublisher httpSessionEventPublisher() { + return new HttpSessionEventPublisher(); + } + +} diff --git a/src/main/java/cn/van/business/controller/BaseController.java b/src/main/java/cn/van/business/controller/BaseController.java new file mode 100644 index 0000000..46a5631 --- /dev/null +++ b/src/main/java/cn/van/business/controller/BaseController.java @@ -0,0 +1,67 @@ +package cn.van.business.controller; + +import cn.van.business.model.user.UserInfo; +import cn.van.business.util.JwtUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; + +import java.util.Collections; +import java.util.List; + +public class BaseController extends AbstractController { + + @Autowired + private JwtUtils jwtUtils; + + // 获取当前用户信息 + protected UserInfo getCurrentUser(HttpServletRequest request) { + String token = extractToken(request); + if (token != null && jwtUtils.validateToken(token)) { + String username = jwtUtils.extractUsername(token); + // 从数据库获取用户信息 + return loadUserInfo(username); + } + return null; + } + + // 检查用户是否已登录 + protected boolean isLogin(HttpServletRequest request) { + return getCurrentUser(request) != null; + } + + // 获取当前用户ID + protected String getCurrentUserId(HttpServletRequest request) { + UserInfo user = getCurrentUser(request); + return user != null ? user.getId() : null; + } + + // 获取当前用户角色 + protected List getCurrentUserRoles(HttpServletRequest request) { + UserInfo user = getCurrentUser(request); + return user != null ? user.getRoles() : Collections.emptyList(); + } + + // 提取token + private String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + // 加载用户信息 + private UserInfo loadUserInfo(String username) { + // 实现用户信息加载 + return new UserInfo(); + } + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + return null; + } +} diff --git a/src/main/java/cn/van/business/controller/jd/OrderController.java b/src/main/java/cn/van/business/controller/jd/OrderController.java index 078ec34..e98e7cc 100644 --- a/src/main/java/cn/van/business/controller/jd/OrderController.java +++ b/src/main/java/cn/van/business/controller/jd/OrderController.java @@ -1,5 +1,6 @@ package cn.van.business.controller.jd; +import cn.van.business.filter.RequireAuth; import cn.van.business.model.ApiResponse; import cn.van.business.model.jd.ProductOrder; import cn.van.business.mq.MessageProducerService; @@ -21,35 +22,18 @@ import java.util.List; */ @RestController @RequestMapping("/recordOrder") +@RequireAuth public class OrderController { public static String TOKEN = "dd7f298cc465e7a9791796ae1303a1f6c031a18d4894ecd9e75d44fdb0ef32d80a49e4c5811b98d8ac65d73d22a54083630d5329c7c9de2276317669ecb2e544"; @Autowired - private JDUtil jdUtils; - @Autowired private MessageProducerService messageProducerService; @Autowired private ProductOrderRepository productOrderRepository; - public boolean checkToken(String token) { - return TOKEN.equals(token); - } - - @RequestMapping("/testToken") - @ResponseBody - public ApiResponse testToken(String token) { - if (!checkToken(token)){ - return ApiResponse.unauthorized(); - } - return ApiResponse.success(); - } - @RequestMapping("/list") @ResponseBody - public ApiResponse list(String token) throws Exception { - if (!checkToken(token)){ - return ApiResponse.unauthorized(); - } + public ApiResponse list() throws Exception { try { List all = productOrderRepository.findAll(); diff --git a/src/main/java/cn/van/business/controller/login/AuthController.java b/src/main/java/cn/van/business/controller/login/AuthController.java new file mode 100644 index 0000000..f24feab --- /dev/null +++ b/src/main/java/cn/van/business/controller/login/AuthController.java @@ -0,0 +1,120 @@ +package cn.van.business.controller.login; + +import cn.van.business.model.user.LoginRequest; +import cn.van.business.model.user.LoginResponse; +import cn.van.business.service.CaptchaService; +import cn.van.business.util.JwtUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private final CaptchaService captchaService; + private final JwtUtils jwtUtils; + private final AuthenticationManager authenticationManager; + private final StringRedisTemplate redisTemplate; + + public AuthController(CaptchaService captchaService, JwtUtils jwtUtils, AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate) { + this.captchaService = captchaService; + this.jwtUtils = jwtUtils; + this.authenticationManager = authenticationManager; + this.redisTemplate = redisTemplate; + } + + private static final String USER_TOKEN_PREFIX = "user:token:"; + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + // 1. 基础校验 + if (StringUtils.isBlank(request.getUsername()) || StringUtils.isBlank(request.getPassword())) { + throw new RuntimeException("用户名或密码不能为空"); + } + + // 2. 验证码校验 + if (!captchaService.validateCaptcha(request.getCaptcha(), request.getGeneratedCaptcha())) { + throw new RuntimeException("验证码错误"); + } +// 在验证码校验后、认证前添加防爆破逻辑 + String loginFailKey = "login:fail:" + request.getUsername(); + Long failCount = redisTemplate.opsForValue().increment(loginFailKey); + if (failCount != null && failCount > 5) { + throw new RuntimeException("尝试次数过多,请稍后再试"); + } + + // 3. 用户认证(Spring Security 标准流程) + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + + // 4. 生成 JWT Token + String token = jwtUtils.generateToken(request.getUsername()); + String refreshToken = generateRefreshToken(request.getUsername()); + + // 5. 存入 Redis + saveUserToken(request.getUsername(), token, jwtUtils.getExpiration(token)); + + // 6. 构造响应 + LoginResponse response = new LoginResponse(); + response.setToken(token); + response.setRefreshToken(refreshToken); + response.setUsername(request.getUsername()); + + return ResponseEntity.ok(response); + } + + + @PostMapping("/logout") + public ResponseEntity logout(@RequestHeader("Authorization") String token) { + String actualToken = token.startsWith("Bearer ") ? token.substring(7) : token; + String username = jwtUtils.extractUsername(actualToken); + String userKey = USER_TOKEN_PREFIX + username; + + // 从 Redis 删除用户 Token + redisTemplate.delete(userKey); + + // 添加 Token 到黑名单 + addToBlacklist(actualToken, jwtUtils.getRemainingExpirationTime(actualToken)); + + return ResponseEntity.ok().build(); + } + + + @GetMapping("/captcha") + public ResponseEntity getCaptcha() throws Exception { + String captchaImage = captchaService.generateCaptchaImage(); + return ResponseEntity.ok(captchaImage); + } + + /** + * 将用户 Token 存入 Redis,并设置相同有效期 + */ + private void saveUserToken(String username, String token, long expiration) { + String key = USER_TOKEN_PREFIX + username; + redisTemplate.opsForValue().set(key, token, expiration, TimeUnit.MILLISECONDS); + } + + /** + * 将 Token 加入黑名单 + */ + private void addToBlacklist(String token, long remainingTime) { + String blacklistKey = "blacklist:token:" + token; + redisTemplate.opsForValue().set(blacklistKey, "invalid", remainingTime, TimeUnit.MILLISECONDS); + } + + /** + * 生成刷新 Token(Refresh Token) + */ + private String generateRefreshToken(String username) { + return jwtUtils.generateToken(username); + } + +} diff --git a/src/main/java/cn/van/business/controller/login/CaptchaController.java b/src/main/java/cn/van/business/controller/login/CaptchaController.java new file mode 100644 index 0000000..a3e5ad6 --- /dev/null +++ b/src/main/java/cn/van/business/controller/login/CaptchaController.java @@ -0,0 +1,24 @@ +package cn.van.business.controller.login; + +import cn.van.business.service.CaptchaService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/captcha") +public class CaptchaController { + + private final CaptchaService captchaService; + + public CaptchaController(CaptchaService captchaService) { + this.captchaService = captchaService; + } + + @GetMapping("/generate") + public ResponseEntity generateCaptcha() throws Exception { + String base64Image = captchaService.generateCaptchaImage(); + return ResponseEntity.ok(base64Image); + } +} diff --git a/src/main/java/cn/van/business/filter/JwtAuthFilter.java b/src/main/java/cn/van/business/filter/JwtAuthFilter.java new file mode 100644 index 0000000..e544d8d --- /dev/null +++ b/src/main/java/cn/van/business/filter/JwtAuthFilter.java @@ -0,0 +1,62 @@ +package cn.van.business.filter; + +import cn.van.business.service.impl.UserDetailsServiceImpl; +import cn.van.business.util.JwtUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +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; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtils jwtUtils; + private final UserDetailsServiceImpl userDetailsService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateToken(jwt)) { + String username = jwtUtils.extractUsername(jwt); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + } catch (Exception e) { + logger.error("无法认证用户: {}"); + } + + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + + return null; + } +} diff --git a/src/main/java/cn/van/business/filter/RequireAuth.java b/src/main/java/cn/van/business/filter/RequireAuth.java new file mode 100644 index 0000000..47d3d81 --- /dev/null +++ b/src/main/java/cn/van/business/filter/RequireAuth.java @@ -0,0 +1,13 @@ +package cn.van.business.filter; + +import org.springframework.security.access.prepost.PreAuthorize; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("hasAnyRole('ADMIN', 'USER')") +public @interface RequireAuth {} diff --git a/src/main/java/cn/van/business/model/user/AdminUser.java b/src/main/java/cn/van/business/model/user/AdminUser.java new file mode 100644 index 0000000..f97da64 --- /dev/null +++ b/src/main/java/cn/van/business/model/user/AdminUser.java @@ -0,0 +1,46 @@ +package cn.van.business.model.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import lombok.Getter; +import lombok.Setter; +import jakarta.persistence.*; + + +import java.time.LocalDateTime; + +/** + * @author Leo + * @version 1.0 + * @create 2025/6/16 10:55 + * @description: + */ +@Getter +@Setter +@Entity +@Table(name = "admin_users") +public class AdminUser { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + private String password; + + private String nickname; + + private String avatarUrl; + + private String roles; // 角色列表,可用逗号分隔 + + private LocalDateTime lastLoginTime; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + diff --git a/src/main/java/cn/van/business/model/user/LoginRequest.java b/src/main/java/cn/van/business/model/user/LoginRequest.java new file mode 100644 index 0000000..479ea74 --- /dev/null +++ b/src/main/java/cn/van/business/model/user/LoginRequest.java @@ -0,0 +1,13 @@ +package cn.van.business.model.user; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + private String username; + private String password; + private String captcha; // 验证码字段(如果需要) + private String generatedCaptcha; +} diff --git a/src/main/java/cn/van/business/model/user/LoginResponse.java b/src/main/java/cn/van/business/model/user/LoginResponse.java new file mode 100644 index 0000000..9981e0f --- /dev/null +++ b/src/main/java/cn/van/business/model/user/LoginResponse.java @@ -0,0 +1,14 @@ +package cn.van.business.model.user; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginResponse { + private String token; + private Long expiresIn; + private String refreshToken; + private UserInfo userInfo; + private String username; +} diff --git a/src/main/java/cn/van/business/model/user/UserInfo.java b/src/main/java/cn/van/business/model/user/UserInfo.java new file mode 100644 index 0000000..619411f --- /dev/null +++ b/src/main/java/cn/van/business/model/user/UserInfo.java @@ -0,0 +1,16 @@ +package cn.van.business.model.user; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class UserInfo { + private String username; + private String nickname; + private String id; + private List roles; + private String avatarUrl; +} diff --git a/src/main/java/cn/van/business/service/AdminUserService.java b/src/main/java/cn/van/business/service/AdminUserService.java new file mode 100644 index 0000000..d69378f --- /dev/null +++ b/src/main/java/cn/van/business/service/AdminUserService.java @@ -0,0 +1,29 @@ +package cn.van.business.service; + +import cn.van.business.model.user.AdminUser; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Service +public class AdminUserService { + + + // 模拟数据库存储 + private static final Map users = new HashMap<>(); + + static { + // 初始化一个测试用户(生产环境应从数据库加载) + AdminUser user = new AdminUser(); + user.setUsername("van"); + user.setPassword(new BCryptPasswordEncoder().encode("LK.807878712")); + users.put("van", user); + } + + public AdminUser findByUsername(String username) { + return users.get(username); + } +} diff --git a/src/main/java/cn/van/business/service/CaptchaService.java b/src/main/java/cn/van/business/service/CaptchaService.java new file mode 100644 index 0000000..ed50b1e --- /dev/null +++ b/src/main/java/cn/van/business/service/CaptchaService.java @@ -0,0 +1,32 @@ +package cn.van.business.service; + +import com.google.code.kaptcha.Producer; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.util.Base64; + +@Service +public class CaptchaService { + + private final Producer kaptchaProducer; + + public CaptchaService(Producer kaptchaProducer) { + this.kaptchaProducer = kaptchaProducer; + } + + public String generateCaptchaImage() throws Exception { + String text = kaptchaProducer.createText(); + BufferedImage image = kaptchaProducer.createImage(text); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", outputStream); + return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + } + + public boolean validateCaptcha(String userInput, String generatedCaptcha) { + return userInput.equalsIgnoreCase(generatedCaptcha); + } +} diff --git a/src/main/java/cn/van/business/service/impl/UserDetailsServiceImpl.java b/src/main/java/cn/van/business/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 0000000..1b0b18d --- /dev/null +++ b/src/main/java/cn/van/business/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,36 @@ +package cn.van.business.service.impl; + +import cn.van.business.model.user.AdminUser; +import cn.van.business.service.AdminUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +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.ArrayList; +import java.util.Collection; +import java.util.Collections; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private AdminUserService adminUserService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + AdminUser user = adminUserService.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("用户不存在"); + } + + return User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .build(); + } + +} diff --git a/src/main/java/cn/van/business/util/AuthEntryPoint.java b/src/main/java/cn/van/business/util/AuthEntryPoint.java new file mode 100644 index 0000000..5fd44ee --- /dev/null +++ b/src/main/java/cn/van/business/util/AuthEntryPoint.java @@ -0,0 +1,18 @@ +package cn.van.business.util; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class AuthEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "需要登录认证"); + } +} diff --git a/src/main/java/cn/van/business/util/JDUtil.java b/src/main/java/cn/van/business/util/JDUtil.java index a3a0a60..7343a31 100644 --- a/src/main/java/cn/van/business/util/JDUtil.java +++ b/src/main/java/cn/van/business/util/JDUtil.java @@ -2058,6 +2058,9 @@ public class JDUtil { public LocalDate getDateFromLD(String dateStr) { logger.info("开始解析日期 {}", dateStr); + if (dateStr.equals("昨天")){ + return getEffectiveToday().minusDays(1).toLocalDate(); + } // 定义支持的日期格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); diff --git a/src/main/java/cn/van/business/util/JwtUtils.java b/src/main/java/cn/van/business/util/JwtUtils.java new file mode 100644 index 0000000..439898a --- /dev/null +++ b/src/main/java/cn/van/business/util/JwtUtils.java @@ -0,0 +1,107 @@ +package cn.van.business.util; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * JWT 工具类 + * 提供 token 的生成、解析、验证和有效期管理功能 + */ +@Component +public class JwtUtils { + + private final String secret = "your-secret-key"; // 应配置在 application.yml 中 + private final long expiration = 86400000 * 7; // 24小时 + + /** + * 生成一个新的 JWT Token + * + * @param username 用户名 + * @param roles 用户角色(可选) + * @return 返回生成的 JWT Token 字符串 + */ +// 生成 Token 时去掉 roles 参数 + public String generateToken(String username) { + return Jwts.builder() + .setSubject(username) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + + /** + * 从给定的 JWT Token 中提取用户名 + * + * @param token JWT Token + * @return 提取的用户名 + */ + public String extractUsername(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + /** + * 获取 JWT Token 的过期时间 + * + * @param token JWT Token + * @return 返回 token 的过期时间 (Date) + */ + public Date extractExpiration(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + } + + /** + * 验证指定的 JWT Token 是否有效 + * + * @param token JWT Token + * @return 如果 token 有效则返回 true,否则返回 false + */ + public Boolean validateToken(String token) { + return !isTokenExpired(token); + } + + + /** + * 检查 token 是否已过期 + * + * @param token JWT Token + * @return 如果已过期返回 true,否则返回 false + */ + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * 获取 JWT Token 的剩余有效时间(毫秒) + * + * @param token JWT Token + * @return 剩余的有效时间(毫秒) + */ + public long getRemainingExpirationTime(String token) { + long now = System.currentTimeMillis(); + return Math.max(0, extractExpiration(token).getTime() - now); + } + + /** + * 获取 JWT Token 的过期时间(毫秒) + * + * @param token JWT Token + * @return 返回 token 的过期时间戳(毫秒) + */ + public long getExpiration(String token) { + return extractExpiration(token).getTime(); + } + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3018c30..5993909 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -8,7 +8,7 @@ spring: #数据源配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://192.168.8.88:3306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8 + url: jdbc:mysql://134.175.126.60:33306/jd?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT%2B8 username: root password: mysql_7sjTXH hikari: @@ -38,10 +38,10 @@ spring: basename: i18n/messages data: redis: - host: 192.168.8.88 + host: 134.175.126.60 password: redis_6PZ52S timeout: 1800000 - port: 6379 + port: 36379 database: 7 # 日志配置 @@ -53,7 +53,7 @@ config: WX_BASE_URL: http://192.168.8.6:7777/qianxun/httpapi?wxid=wxid_kr145nk7l0an31 QL_BASE_URL: http://134.175.126.60:35700 rocketmq: - name-server: 192.168.8.88:9876 # RocketMQ Name Server 地址 + name-server: 192.168.8.88:39876 # RocketMQ Name Server 地址 producer: group: wx_producer # 生产者组名 send-msg-timeout: 1000 # 发送消息超时时间 @@ -63,5 +63,5 @@ rocketmq: consume-thread-max: 64 # 消费线程池最大线程数 consume-message-batch-max-size: 64 # 批量消费最大消息数 isRunning: - wx: true - jd: true + wx: false + jd: false