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