From de52e2816c5bef912be8b745d290699d5aee51ea Mon Sep 17 00:00:00 2001
From: Hanserwei <2628273921@qq.com>
Date: Sat, 29 Nov 2025 12:00:30 +0800
Subject: [PATCH] feat(jwt): implement JWT-based authentication system
---
.idea/compiler.xml | 4 +-
.idea/gradle.xml | 1 +
.idea/modules.xml | 3 +-
gradle/libs.versions.toml | 10 ++
settings.gradle.kts | 3 +-
weblog-module-admin/build.gradle.kts | 5 +
.../admin/config/WebSecurityConfig.java | 61 ++++++++++
weblog-module-common/build.gradle.kts | 2 +-
.../domain/repository/UserRepository.java | 1 +
.../common/enums/ResponseCodeEnum.java | 2 +
weblog-module-jwt/build.gradle.kts | 16 +++
.../JwtAuthenticationSecurityConfig.java | 39 +++++++
.../jwt/config/PasswordEncoderConfig.java | 20 ++++
.../UsernameOrPasswordNullException.java | 14 +++
.../jwt/filter/JwtAuthenticationFilter.java | 47 ++++++++
.../RestAuthenticationFailureHandler.java | 37 ++++++
.../RestAuthenticationSuccessHandler.java | 40 +++++++
.../com/hanserwei/jwt/model/LoginRspVO.java | 19 +++
.../jwt/service/UserDetailServiceImpl.java | 36 ++++++
.../hanserwei/jwt/utils/JwtTokenHelper.java | 110 ++++++++++++++++++
.../com/hanserwei/jwt/utils/ResultUtil.java | 67 +++++++++++
.../web/controller/TestController.java | 4 +-
.../src/main/resources/config/application.yml | 7 +-
23 files changed, 540 insertions(+), 8 deletions(-)
create mode 100644 gradle/libs.versions.toml
create mode 100644 weblog-module-admin/src/main/java/com/hanserwei/admin/config/WebSecurityConfig.java
create mode 100644 weblog-module-jwt/build.gradle.kts
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/JwtAuthenticationSecurityConfig.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/PasswordEncoderConfig.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/exception/UsernameOrPasswordNullException.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/filter/JwtAuthenticationFilter.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationFailureHandler.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationSuccessHandler.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/model/LoginRspVO.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/service/UserDetailServiceImpl.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/JwtTokenHelper.java
create mode 100644 weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/ResultUtil.java
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index dc0d1fa..ff8279c 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -8,10 +8,12 @@
+
-
+
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 5fe35d7..ae1e2a3 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -10,6 +10,7 @@
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index ac138a8..a93abb3 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,9 +3,8 @@
+
-
-
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..152a132
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,10 @@
+[versions]
+# 定义版本号
+jjwt = "0.13.0"
+
+[libraries]
+# 定义依赖包的别名 (bundles 是可选的,libraries 是必须的)
+# 对应 Maven 的 io.jsonwebtoken
+jjwt-api = { group = "io.jsonwebtoken", name = "jjwt-api", version.ref = "jjwt" }
+jjwt-impl = { group = "io.jsonwebtoken", name = "jjwt-impl", version.ref = "jjwt" }
+jjwt-jackson = { group = "io.jsonwebtoken", name = "jjwt-jackson", version.ref = "jjwt" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7871e04..b230bf8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,4 +2,5 @@ rootProject.name = "weblog-springboot"
include("weblog-web")
include("weblog-module-admin")
-include("weblog-module-common")
\ No newline at end of file
+include("weblog-module-common")
+include("weblog-module-jwt")
\ No newline at end of file
diff --git a/weblog-module-admin/build.gradle.kts b/weblog-module-admin/build.gradle.kts
index 7517d54..9c9b99c 100644
--- a/weblog-module-admin/build.gradle.kts
+++ b/weblog-module-admin/build.gradle.kts
@@ -3,8 +3,13 @@ plugins {
}
dependencies {
+ // Spring Security
+ implementation("org.springframework.boot:spring-boot-starter-security")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation(project(":weblog-module-common"))
+
+ implementation(project(":weblog-module-jwt"))
+ testImplementation("org.springframework.security:spring-security-test")
}
\ No newline at end of file
diff --git a/weblog-module-admin/src/main/java/com/hanserwei/admin/config/WebSecurityConfig.java b/weblog-module-admin/src/main/java/com/hanserwei/admin/config/WebSecurityConfig.java
new file mode 100644
index 0000000..eca740a
--- /dev/null
+++ b/weblog-module-admin/src/main/java/com/hanserwei/admin/config/WebSecurityConfig.java
@@ -0,0 +1,61 @@
+package com.hanserwei.admin.config;
+
+import com.hanserwei.jwt.config.JwtAuthenticationSecurityConfig;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig {
+
+ @Resource
+ private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ // 1. 禁用 CSRF
+ .csrf(AbstractHttpConfigurer::disable)
+ // 2. 禁用表单登录
+ .formLogin(AbstractHttpConfigurer::disable)
+ // 3. 授权规则配置
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/admin/**").authenticated() // 保护 /admin/**
+ .anyRequest().permitAll() // 其他放行
+ )
+ // 4. 会话管理:无状态
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ // 5. 应用自定义配置 (核心变化)
+ .with(jwtAuthenticationSecurityConfig, Customizer.withDefaults());
+ return http.build();
+ }
+
+ /**
+ * 定义 AuthenticationProvider Bean。
+ * Spring Security 会自动将此 Provider 注入到 AuthenticationManager 中。
+ */
+ @Bean
+ public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
+ PasswordEncoder passwordEncoder) {
+ // 修正点:直接在构造函数中传入 userDetailsService
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
+
+ // 设置密码编码器 (PasswordEncoder 依然使用 setter 设置)
+ authProvider.setPasswordEncoder(passwordEncoder);
+
+ return authProvider;
+ }
+}
diff --git a/weblog-module-common/build.gradle.kts b/weblog-module-common/build.gradle.kts
index 11a2442..03d0289 100644
--- a/weblog-module-common/build.gradle.kts
+++ b/weblog-module-common/build.gradle.kts
@@ -18,7 +18,7 @@ dependencies {
// aop
implementation("org.springframework.boot:spring-boot-starter-aop")
// web
- implementation("org.springframework.boot:spring-boot-starter-web")
+ api("org.springframework.boot:spring-boot-starter-web")
// postgresql
runtimeOnly("org.postgresql:postgresql")
}
diff --git a/weblog-module-common/src/main/java/com/hanserwei/common/domain/repository/UserRepository.java b/weblog-module-common/src/main/java/com/hanserwei/common/domain/repository/UserRepository.java
index 1111a54..9042706 100644
--- a/weblog-module-common/src/main/java/com/hanserwei/common/domain/repository/UserRepository.java
+++ b/weblog-module-common/src/main/java/com/hanserwei/common/domain/repository/UserRepository.java
@@ -4,4 +4,5 @@ import com.hanserwei.common.domain.dataobject.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository {
+ User getUsersByUsername(String username);
}
diff --git a/weblog-module-common/src/main/java/com/hanserwei/common/enums/ResponseCodeEnum.java b/weblog-module-common/src/main/java/com/hanserwei/common/enums/ResponseCodeEnum.java
index b3b3a6c..02e7128 100644
--- a/weblog-module-common/src/main/java/com/hanserwei/common/enums/ResponseCodeEnum.java
+++ b/weblog-module-common/src/main/java/com/hanserwei/common/enums/ResponseCodeEnum.java
@@ -13,6 +13,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
PARAM_NOT_VALID("10001", "参数错误"),
// ----------- 业务异常状态码 -----------
+ LOGIN_FAIL("20000", "登录失败"),
+ USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
;
// 异常码
diff --git a/weblog-module-jwt/build.gradle.kts b/weblog-module-jwt/build.gradle.kts
new file mode 100644
index 0000000..bcb19b2
--- /dev/null
+++ b/weblog-module-jwt/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ `java-library`
+}
+
+group = "com.hanserwei.jwt"
+version = project.parent?.version ?: "0.0.1-SNAPSHOT"
+
+dependencies {
+ implementation("org.springframework.boot:spring-boot-starter-security")
+ implementation(project(":weblog-module-common"))
+
+ // jwt
+ api(libs.jjwt.api)
+ runtimeOnly(libs.jjwt.impl)
+ runtimeOnly(libs.jjwt.jackson)
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/JwtAuthenticationSecurityConfig.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/JwtAuthenticationSecurityConfig.java
new file mode 100644
index 0000000..89e4c97
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/JwtAuthenticationSecurityConfig.java
@@ -0,0 +1,39 @@
+package com.hanserwei.jwt.config;
+
+import com.hanserwei.jwt.filter.JwtAuthenticationFilter;
+import com.hanserwei.jwt.handler.RestAuthenticationFailureHandler;
+import com.hanserwei.jwt.handler.RestAuthenticationSuccessHandler;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter {
+
+ @Resource
+ private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler;
+
+ @Resource
+ private RestAuthenticationFailureHandler restAuthenticationFailureHandler;
+
+ @Override
+ public void configure(HttpSecurity httpSecurity) {
+ // 1. 实例化自定义过滤器
+ JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
+
+ // 2. 这里的 AuthenticationManager 是从外部传入的 (httpSecurity sharedObject)
+ filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
+
+ // 3. 设置成功/失败处理器
+ filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler);
+ filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler);
+
+ // 4. 将过滤器添加到 UsernamePasswordAuthenticationFilter 之前
+ // (JWT 校验通常在用户名密码校验之前,或者用来替换它,addFilterBefore 是最稳妥的)
+ httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/PasswordEncoderConfig.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/PasswordEncoderConfig.java
new file mode 100644
index 0000000..caaeb23
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/config/PasswordEncoderConfig.java
@@ -0,0 +1,20 @@
+package com.hanserwei.jwt.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class PasswordEncoderConfig {
+ public static void main(String[] args) {
+ BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+ System.out.println(encoder.encode("hanserwei"));
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ // BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/exception/UsernameOrPasswordNullException.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/exception/UsernameOrPasswordNullException.java
new file mode 100644
index 0000000..c5d0dda
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/exception/UsernameOrPasswordNullException.java
@@ -0,0 +1,14 @@
+package com.hanserwei.jwt.exception;
+
+
+import org.springframework.security.core.AuthenticationException;
+
+public class UsernameOrPasswordNullException extends AuthenticationException {
+ public UsernameOrPasswordNullException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ public UsernameOrPasswordNullException(String msg) {
+ super(msg);
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/filter/JwtAuthenticationFilter.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..ddefdfa
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,47 @@
+package com.hanserwei.jwt.filter;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.hanserwei.jwt.exception.UsernameOrPasswordNullException;
+import io.micrometer.common.util.StringUtils;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.RegexRequestMatcher;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
+
+ public JwtAuthenticationFilter() {
+ super(new RegexRequestMatcher("/login", HttpMethod.POST.name()));
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ // 解析提交的JSON数据
+ JsonNode jsonNode = mapper.readTree(request.getInputStream());
+ JsonNode usernameNode = jsonNode.get("username");
+ JsonNode passwordNode = jsonNode.get("password");
+ // 判断用户名、密码是否为空
+ if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode)
+ || StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) {
+ throw new UsernameOrPasswordNullException("用户名或密码不能为空");
+ }
+
+ String username = usernameNode.textValue();
+ String password = passwordNode.textValue();
+
+ // 将用户名、密码封装到 Token 中
+ UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
+ = new UsernamePasswordAuthenticationToken(username, password);
+ return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationFailureHandler.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationFailureHandler.java
new file mode 100644
index 0000000..9f05e75
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationFailureHandler.java
@@ -0,0 +1,37 @@
+package com.hanserwei.jwt.handler;
+
+import com.hanserwei.common.enums.ResponseCodeEnum;
+import com.hanserwei.common.utils.Response;
+import com.hanserwei.jwt.exception.UsernameOrPasswordNullException;
+import com.hanserwei.jwt.utils.ResultUtil;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Slf4j
+@Component
+public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
+ log.warn("AuthenticationException: ", exception);
+ if (exception instanceof UsernameOrPasswordNullException) {
+ // 用户名或密码为空
+ ResultUtil.fail(response, Response.fail(exception.getMessage()));
+ return;
+ } else if (exception instanceof BadCredentialsException) {
+ // 用户名或密码错误
+ ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR));
+ return;
+ }
+
+ // 登录失败
+ ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL));
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationSuccessHandler.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationSuccessHandler.java
new file mode 100644
index 0000000..9a28414
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/handler/RestAuthenticationSuccessHandler.java
@@ -0,0 +1,40 @@
+package com.hanserwei.jwt.handler;
+
+import com.hanserwei.common.utils.Response;
+import com.hanserwei.jwt.model.LoginRspVO;
+import com.hanserwei.jwt.utils.JwtTokenHelper;
+import com.hanserwei.jwt.utils.ResultUtil;
+import jakarta.annotation.Resource;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Slf4j
+@Component
+public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+ @Resource
+ private JwtTokenHelper jwtTokenHelper;
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+ // 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名
+ UserDetails userDetails = (UserDetails) authentication.getPrincipal();
+
+ // 通过用户名生成 Token
+ String username = userDetails.getUsername();
+ String token = jwtTokenHelper.generateToken(username);
+
+ // 返回 Token
+ LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build();
+
+ ResultUtil.ok(response, Response.success(loginRspVO));
+ }
+}
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/model/LoginRspVO.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/model/LoginRspVO.java
new file mode 100644
index 0000000..7b24648
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/model/LoginRspVO.java
@@ -0,0 +1,19 @@
+package com.hanserwei.jwt.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class LoginRspVO {
+
+ /**
+ * Token 值
+ */
+ private String token;
+
+}
\ No newline at end of file
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/service/UserDetailServiceImpl.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/service/UserDetailServiceImpl.java
new file mode 100644
index 0000000..4f521c2
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/service/UserDetailServiceImpl.java
@@ -0,0 +1,36 @@
+package com.hanserwei.jwt.service;
+
+import com.hanserwei.common.domain.dataobject.User;
+import com.hanserwei.common.domain.repository.UserRepository;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+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.Objects;
+
+@Service
+@Slf4j
+public class UserDetailServiceImpl implements UserDetailsService {
+
+ @Resource
+ private UserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ // 从数据库查询用户信息
+ User user = userRepository.getUsersByUsername(username);
+ // 判断用户是否存在
+ if (Objects.isNull(user)) {
+ throw new UsernameNotFoundException("用户不存在");
+ }
+
+ // authorities 用于指定角色,这里写死为 ADMIN 管理员
+ return org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
+ .password(user.getPassword())
+ .authorities("ADMIN")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/JwtTokenHelper.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/JwtTokenHelper.java
new file mode 100644
index 0000000..ccb3f3d
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/JwtTokenHelper.java
@@ -0,0 +1,110 @@
+package com.hanserwei.jwt.utils;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.io.Encoders;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+
+@Component
+public class JwtTokenHelper implements InitializingBean {
+
+ /**
+ * 签发人
+ */
+ @Value("${jwt.issuer}")
+ private String issuer;
+
+ /**
+ * 秘钥 (JJWT 0.12+ 强制要求使用 SecretKey 接口,且长度必须符合算法安全标准)
+ */
+ private SecretKey key;
+
+ /**
+ * JWT 解析器 (线程安全,建议复用)
+ */
+ private JwtParser jwtParser;
+
+ /**
+ * 工具方法:生成一个符合 HS512 标准的安全 Base64 秘钥
+ * 用于生成后填入 application.yml
+ */
+ public static String generateBase64Key() {
+ SecretKey secretKey = Jwts.SIG.HS512.key().build();
+ return Encoders.BASE64.encode(secretKey.getEncoded()); // Fixed line
+ }
+
+ public static void main(String[] args) {
+ String key = generateBase64Key();
+ System.out.println("请将此 Key 复制到配置文件中 (HS512需要长Key): " + key);
+ }
+
+ /**
+ * 设置 Base64 秘钥
+ * 官方建议:HS512 算法至少需要 512 bit (64字节) 的秘钥长度,否则会抛出 WeakKeyException
+ */
+ @Value("${jwt.secret}")
+ public void setBase64Key(String base64Key) {
+ // 使用 JJWT 提供的 Decoders 工具,或者使用 java.util.Base64 均可
+ byte[] keyBytes = Decoders.BASE64.decode(base64Key);
+ this.key = Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ /**
+ * 初始化 JwtParser
+ */
+ @Override
+ public void afterPropertiesSet() {
+ // 0.13.0 标准写法:
+ // 1. 使用 parser() 而不是 parserBuilder()
+ // 2. 使用 verifyWith() 设置验签秘钥
+ jwtParser = Jwts.parser()
+ .requireIssuer(issuer)
+ .verifyWith(key)
+ // 允许的时钟偏差 (防止服务器时间不一致导致验证失败),默认单位秒
+ .clockSkewSeconds(10)
+ .build();
+ }
+
+ /**
+ * 生成 Token
+ */
+ public String generateToken(String username) {
+ Instant now = Instant.now();
+ Instant expireTime = now.plus(1, ChronoUnit.HOURS);
+
+ return Jwts.builder()
+ .header().add("type", "JWT").and() // 推荐添加 header
+ .subject(username)
+ .issuer(issuer)
+ .issuedAt(Date.from(now))
+ .expiration(Date.from(expireTime))
+ // 0.13.0 标准:使用 Jwts.SIG 中的常量指定算法
+ .signWith(key, Jwts.SIG.HS512)
+ .compact();
+ }
+
+ /**
+ * 解析 Token
+ */
+ public Jws parseToken(String token) {
+ try {
+ // 0.13.0 标准:解析已签名的 JWS 使用 parseSignedClaims
+ return jwtParser.parseSignedClaims(token);
+ } catch (MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
+ // 注意:SignatureException 现在属于 io.jsonwebtoken.security 包
+ throw new BadCredentialsException("Token 不可用或签名无效", e);
+ } catch (ExpiredJwtException e) {
+ throw new CredentialsExpiredException("Token 已失效", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/ResultUtil.java b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/ResultUtil.java
new file mode 100644
index 0000000..14013ab
--- /dev/null
+++ b/weblog-module-jwt/src/main/java/com/hanserwei/jwt/utils/ResultUtil.java
@@ -0,0 +1,67 @@
+package com.hanserwei.jwt.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.hanserwei.common.utils.Response;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class ResultUtil {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 成功响应
+ *
+ * @param response HttpServletResponse对象
+ * @param result 响应数据
+ * @throws IOException IO异常
+ */
+ public static void ok(HttpServletResponse response, Response> result) throws IOException {
+ writeResponse(response, HttpStatus.OK.value(), result);
+ }
+
+ /**
+ * 失败响应
+ *
+ * @param response HttpServletResponse对象
+ * @param result 响应数据
+ * @throws IOException IO异常
+ */
+ public static void fail(HttpServletResponse response, Response> result) throws IOException {
+ writeResponse(response, HttpStatus.OK.value(), result);
+ }
+
+ /**
+ * 失败响应
+ *
+ * @param response HttpServletResponse对象
+ * @param status HTTP状态码
+ * @param result 响应数据
+ * @throws IOException IO异常
+ */
+ public static void fail(HttpServletResponse response, int status, Response> result) throws IOException {
+ writeResponse(response, status, result);
+ }
+
+ /**
+ * 写入响应数据
+ *
+ * @param response HttpServletResponse对象
+ * @param status HTTP状态码
+ * @param result 响应数据
+ * @throws IOException IO异常
+ */
+ private static void writeResponse(HttpServletResponse response, int status, Response> result) throws IOException {
+ response.setCharacterEncoding("UTF-8");
+ response.setStatus(status);
+ response.setContentType("application/json");
+
+ try (PrintWriter writer = response.getWriter()) {
+ writer.write(objectMapper.writeValueAsString(result));
+ writer.flush();
+ }
+ }
+}
diff --git a/weblog-web/src/main/java/com/hanserwei/web/controller/TestController.java b/weblog-web/src/main/java/com/hanserwei/web/controller/TestController.java
index b85c902..4735912 100644
--- a/weblog-web/src/main/java/com/hanserwei/web/controller/TestController.java
+++ b/weblog-web/src/main/java/com/hanserwei/web/controller/TestController.java
@@ -17,9 +17,9 @@ import java.util.stream.Collectors;
@Slf4j
public class TestController {
- @PostMapping("/test")
+ @PostMapping("/admin/test")
@ApiOperationLog(description = "测试接口")
- public ResponseEntity test(@RequestBody @Validated User user, BindingResult bindingResult) {
+ public ResponseEntitytest(@RequestBody @Validated User user, BindingResult bindingResult) {
// 是否存在校验错误
if (bindingResult.hasErrors()) {
// 获取校验不通过字段的提示信息
diff --git a/weblog-web/src/main/resources/config/application.yml b/weblog-web/src/main/resources/config/application.yml
index f0c5452..46e5773 100644
--- a/weblog-web/src/main/resources/config/application.yml
+++ b/weblog-web/src/main/resources/config/application.yml
@@ -2,4 +2,9 @@ spring:
application:
name: han-blog
profiles:
- active: dev
\ No newline at end of file
+ active: dev
+jwt:
+ # 签发人
+ issuer: Hanserwei
+ # 秘钥
+ secret: P81m2EjMkZj74ht+OuBCxsf25if8PzghbEEWASyf4zcYnxwwn3VbCiIohxmowYg/8I4mj6eJdaqLbJEhggUq3Q==
\ No newline at end of file