feat(jwt): implement JWT-based authentication system
This commit is contained in:
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@@ -8,10 +8,12 @@
|
||||
<processorPath useClasspath="false">
|
||||
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.42/8365263844ebb62398e0dc33057ba10ba472d3b8/lombok-1.18.42.jar" />
|
||||
</processorPath>
|
||||
<module name="weblog-springboot.weblog-module-jwt.main" />
|
||||
<module name="weblog-springboot.weblog-web.test" />
|
||||
<module name="weblog-springboot.weblog-module-admin.test" />
|
||||
<module name="weblog-springboot.weblog-module-common.test" />
|
||||
<module name="weblog-springboot.weblog-module-admin.main" />
|
||||
<module name="weblog-springboot.weblog-module-jwt.test" />
|
||||
<module name="weblog-springboot.weblog-module-admin.test" />
|
||||
<module name="weblog-springboot.weblog-module-common.main" />
|
||||
<module name="weblog-springboot.weblog-web.main" />
|
||||
</profile>
|
||||
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -10,6 +10,7 @@
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/weblog-module-admin" />
|
||||
<option value="$PROJECT_DIR$/weblog-module-common" />
|
||||
<option value="$PROJECT_DIR$/weblog-module-jwt" />
|
||||
<option value="$PROJECT_DIR$/weblog-web" />
|
||||
</set>
|
||||
</option>
|
||||
|
||||
3
.idea/modules.xml
generated
3
.idea/modules.xml
generated
@@ -3,9 +3,8 @@
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/weblog-springboot.iml" filepath="$PROJECT_DIR$/weblog-springboot.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-admin/weblog-springboot.weblog-module-admin.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-admin/weblog-springboot.weblog-module-admin.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-module-common/weblog-springboot.weblog-module-common.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.test.iml" filepath="$PROJECT_DIR$/.idea/modules/weblog-web/weblog-springboot.weblog-web.test.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
10
gradle/libs.versions.toml
Normal file
10
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[versions]
|
||||
# 定义版本号
|
||||
jjwt = "0.13.0"
|
||||
|
||||
[libraries]
|
||||
# 定义依赖包的别名 (bundles 是可选的,libraries 是必须的)
|
||||
# 对应 Maven 的 <groupId>io.jsonwebtoken</groupId>
|
||||
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" }
|
||||
@@ -3,3 +3,4 @@ rootProject.name = "weblog-springboot"
|
||||
include("weblog-web")
|
||||
include("weblog-module-admin")
|
||||
include("weblog-module-common")
|
||||
include("weblog-module-jwt")
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ import com.hanserwei.common.domain.dataobject.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
User getUsersByUsername(String username);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
PARAM_NOT_VALID("10001", "参数错误"),
|
||||
|
||||
// ----------- 业务异常状态码 -----------
|
||||
LOGIN_FAIL("20000", "登录失败"),
|
||||
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
16
weblog-module-jwt/build.gradle.kts
Normal file
16
weblog-module-jwt/build.gradle.kts
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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<DefaultSecurityFilterChain, HttpSecurity> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Claims> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
public class TestController {
|
||||
|
||||
@PostMapping("/test")
|
||||
@PostMapping("/admin/test")
|
||||
@ApiOperationLog(description = "测试接口")
|
||||
public ResponseEntity<String>test(@RequestBody @Validated User user, BindingResult bindingResult) {
|
||||
// 是否存在校验错误
|
||||
|
||||
@@ -3,3 +3,8 @@ spring:
|
||||
name: han-blog
|
||||
profiles:
|
||||
active: dev
|
||||
jwt:
|
||||
# 签发人
|
||||
issuer: Hanserwei
|
||||
# 秘钥
|
||||
secret: P81m2EjMkZj74ht+OuBCxsf25if8PzghbEEWASyf4zcYnxwwn3VbCiIohxmowYg/8I4mj6eJdaqLbJEhggUq3Q==
|
||||
Reference in New Issue
Block a user