This commit is contained in:
wangjianhong
2025-07-14 08:33:27 +08:00
parent f66a4ad4bd
commit 1ee4f97ec2
20 changed files with 125 additions and 618 deletions

33
pom.xml
View File

@@ -37,6 +37,18 @@
</properties>
<dependencies>
<!-- 通用认证模块 -->
<dependency>
<groupId>com.arrokoth.framework</groupId>
<artifactId>basic-authorization-server</artifactId>
<version>1.0.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -56,6 +68,18 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<exclusions>
<exclusion>
<artifactId>nimbus-jose-jwt</artifactId>
<groupId>com.nimbusds</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
@@ -70,7 +94,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version> <!-- 根据需要选择最新版本 -->
<version>2.12.1</version> <!-- 根据需要选择最新版本 -->
</dependency>
<dependency>
@@ -84,13 +108,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Spring Session Data Redis -->
<dependency>
<groupId>org.springframework.session</groupId>

View File

@@ -1,8 +1,12 @@
package com.arrokoth.standalone.authorization;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@MapperScan("com.arrokoth.basic.mapper")
@ComponentScan(basePackages = "com.arrokoth")
@SpringBootApplication
public class StandaloneServerApplication {

View File

@@ -1,7 +1,7 @@
package com.arrokoth.standalone.authorization.config;
import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties;
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
import com.arrokoth.basic.properties.AuthorizationServerProperties;
import com.arrokoth.basic.properties.SecurityWebProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

View File

@@ -1,10 +1,12 @@
package com.arrokoth.standalone.authorization.config;
import com.arrokoth.basic.autoconfiguration.BasicAutoConfiguration;
import com.arrokoth.basic.properties.SecurityWebProperties;
import com.arrokoth.standalone.authorization.filter.JwtRequestFilter;
import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties;
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -25,34 +27,12 @@ import java.util.List;
@Slf4j
@Configuration
@AutoConfigureBefore(BasicAutoConfiguration.class)
@AllArgsConstructor
@EnableConfigurationProperties(SecurityWebProperties.class)
public class SecurityWebAutoConfigurer {
private final SecurityWebProperties securityWebProperties;
private final AuthorizationServerProperties authorizationServerProperties;
// @Bean
// public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// http
// .securityMatcher("/**")
// .authorizeHttpRequests(auth -> auth
// .requestMatchers(HttpMethod.OPTIONS).permitAll()
// .requestMatchers(securityWebProperties.getMergedPermittedUrls().toArray(new String[0])).permitAll()
// .anyRequest().authenticated()
// )
// .formLogin(form -> form
// .loginPage(authorizationServerProperties.getLoginPage())
// .permitAll()
// )
// .logout(logout -> logout
// .logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl())
// .permitAll()
// );
//
// return http.build();
// }
private final JwtRequestFilter jwtRequestFilter; // 你的 Token 过滤器

View File

@@ -1,19 +1,11 @@
package com.arrokoth.standalone.authorization.controller;
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
import com.arrokoth.standalone.authorization.domain.response.RestResponse;
import com.arrokoth.standalone.authorization.domain.response.Token;
import com.arrokoth.standalone.authorization.properties.SecurityWebProperties;
import com.arrokoth.standalone.authorization.service.AuthorizationService;
import com.arrokoth.standalone.authorization.service.OAuth2ConsentService;
import com.arrokoth.standalone.authorization.util.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
@@ -23,7 +15,7 @@ import java.security.Principal;
public class AuthorizationController {
private final AuthorizationService authorizationService;
private final OAuth2ConsentService oAuth2ConsentService;
@@ -32,30 +24,8 @@ public class AuthorizationController {
return "login"; // 对应templates/login.html
}
@PostMapping(SecurityWebProperties.AXIOS_LOGIN_PROCESSING_URL)
@ResponseBody
public RestResponse<Token> axiosLogin(@RequestBody LoginRequest loginRequest) {
return RestResponse.success(authorizationService.login(loginRequest));
}
@GetMapping("/user/info")
@ResponseBody
public RestResponse<BasicUser> getBasicUser() {
return RestResponse.success(authorizationService.getBasicUser());
}
@PostMapping("/logout")
@ResponseBody
public ResponseEntity<?> logout(HttpServletRequest request) throws Exception {
String token = JwtUtils.extractTokenFromHeader(request);
authorizationService.logout(JwtUtils.extractJti(token));
return ResponseEntity.ok("Logged out successfully");
}
@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,
@RequestParam("client_id") String clientId,

View File

@@ -1,11 +0,0 @@
package com.arrokoth.standalone.authorization.domain.request;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}

View File

@@ -1,14 +0,0 @@
package com.arrokoth.standalone.authorization.domain.response;
import lombok.Data;
import java.util.List;
@Data
public class BasicUser {
private String username;
private String avatar;
private String introduction;
private List<String> roles;
}

View File

@@ -1,37 +0,0 @@
package com.arrokoth.standalone.authorization.domain.response;
import lombok.Data;
@Data
public class RestResponse<T> {
private String code;
private String message;
private Long timestamp;
private T data;
// 默认构造函数
public RestResponse() {
this.timestamp = System.currentTimeMillis();
}
// 全参构造函数
public RestResponse(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
// 为了方便使用,可以添加一些静态方法来创建实例
public static <T> RestResponse<T> success(T data) {
return new RestResponse<>("200", "success", data);
}
public static <T> RestResponse<T> error(String code, String message) {
return new RestResponse<>(code, message, null);
}
}

View File

@@ -1,13 +0,0 @@
package com.arrokoth.standalone.authorization.domain.response;
import lombok.Data;
@Data
public class Token {
private String access_token;
private String token_type;
private Long expires_in;
private String refresh_token;
}

View File

@@ -1,7 +1,7 @@
package com.arrokoth.standalone.authorization.filter;
import com.arrokoth.basic.util.JwtUtils;
import com.arrokoth.standalone.authorization.store.redis.RedisTokenService;
import com.arrokoth.standalone.authorization.util.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -51,9 +51,9 @@ public class JwtRequestFilter extends OncePerRequestFilter {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (JwtUtils.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
if (JwtUtils.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

View File

@@ -1,60 +0,0 @@
package com.arrokoth.standalone.authorization.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 授权服务器相关配置属性
*/
@Data
@ConfigurationProperties(prefix = "arrokoth.authorization.server")
public class AuthorizationServerProperties {
private static final String DEFAULT_LOGIN_PAGE = "/login";
private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout";
private static final List<String> DEFAULT_PERMIT_URLS = Arrays.asList(
"/login", "/logout", "/connect/logout",
"/assets/**", "/static/**", "/webjars/**", "/error", "/oauth2/**"
);
/**
* 授权确认页面路径,默认为 "/oauth2/consent"
*/
private String consentPage = "/oauth2/consent";
/**
* OAuth2 授权端点路径,默认为 "/oauth2/authorize"
*/
private String authorizationEndpoint = "/oauth2/authorize";
/**
* JWT Issuer 值,默认为 "https://www.arrokoth-info.com"
*/
private String issuer = "https://www.arrokoth-info.com";
/**
* 登出成功后的跳转 URL。
* 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。
* 如果在 application.yml 中配置了 app.security.logout-success-url则使用配置值
* 否则使用该默认值。
*/
private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL;
/**
* 需要放行(无需认证即可访问)的 URL 列表。
* 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。
*/
private List<String> permitUrls = new ArrayList<>();
/**
* 合并默认值与自定义配置,并去重
*/
public List<String> getMergedPermittedUrls() {
return Stream.concat(DEFAULT_PERMIT_URLS.stream(), permitUrls.stream())
.distinct()
.collect(Collectors.toList());
}
}

View File

@@ -1,89 +0,0 @@
package com.arrokoth.standalone.authorization.properties;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 授权服务器相关配置属性
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "arrokoth.security.web")
public class SecurityWebProperties {
public static final String AXIOS_LOGIN_PROCESSING_URL = "/home/login";
public static final String DEFAULT_LOGIN_PROCESSING_URL = "/web/login";
private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout";
private static final String DEFAULT_LOGOUT_PAGE = "/login";
private static final List<String> DEFAULT_PERMIT_URLS = Arrays.asList(
DEFAULT_LOGIN_PROCESSING_URL,
"/login", "/logout", "/connect/logout", "/home/login",
"/assets/**", "/static/**", "/webjars/**",
"/actuator/**",
"/error", "/oauth2/**"
);
private static final List<String> SWAGGER_URLS = Arrays.asList(
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/**",
"/doc.html",
"/swagger-ui.html"
);
/**
* 登出成功后的跳转 URL。
* 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。
* 如果在 application.yml 中配置了 app.security.logout-success-url则使用配置值
* 否则使用该默认值。
*/
private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL;
private String loginPage = DEFAULT_LOGOUT_PAGE;
/**
* 处理登录请求
*/
private String loginProcessingUrl = DEFAULT_LOGIN_PROCESSING_URL;
/**
* 需要放行(无需认证即可访问)的 URL 列表。
* 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。
*/
private List<String> permitUrls = new ArrayList<>();
/**
* 合并默认值与自定义配置,并去重
*/
public List<String> getMergedPermittedUrls() {
List<String> merged = Stream.of(DEFAULT_PERMIT_URLS, SWAGGER_URLS, permitUrls)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
log.info("Merged permitted urls: {}", JSONUtil.toJsonStr(merged));
return merged;
}
}

View File

@@ -1,15 +0,0 @@
package com.arrokoth.standalone.authorization.service;
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
import com.arrokoth.standalone.authorization.domain.response.Token;
public interface AuthorizationService {
Token login(LoginRequest loginRequest);
void logout(String jti);
BasicUser getBasicUser();
}

View File

@@ -1,17 +1,70 @@
package com.arrokoth.standalone.authorization.service;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.util.Set;
import java.util.*;
public interface OAuth2ConsentService {
/**
* OAuth2 授权确认服务实现类
*/
@Service
@RequiredArgsConstructor
public class OAuth2ConsentService {
OAuth2ConsentService.ConsentResponse getConsentDetails(Principal principal,
String clientId,
String scope,
String state,
String principalName);
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
/**
* 获取用户授权的详细信息,包括需要审批和已审批的 scopes
*
* @param clientId 客户端 ID
* @param scope 请求的 scope 列表(空格分隔)
* @param principalName 用户名
* @return 包含待批准和已批准 scopes 的 ConsentResult 对象
*/
public ConsentResponse getConsentDetails(Principal principal,
String clientId,
String scope,
String state,
String principalName) {
Set<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
RegisteredClient registeredClient = Optional.ofNullable(
registeredClientRepository.findByClientId(clientId))
.orElseThrow(() -> new IllegalArgumentException("客户端不存在"));
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principalName);
Set<String> authorizedScopes = Optional.ofNullable(currentAuthorizationConsent)
.map(OAuth2AuthorizationConsent::getScopes)
.orElse(Set.of());
Arrays.stream(StringUtils.delimitedListToStringArray(scope, " "))
.filter(requestedScope -> !OidcScopes.OPENID.equals(requestedScope))
.forEach(requestedScope -> {
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
});
return new ConsentResponse(
Collections.unmodifiableSet(scopesToApprove),
Collections.unmodifiableSet(previouslyApprovedScopes),
registeredClient
);
}
/**
@@ -21,10 +74,11 @@ public interface OAuth2ConsentService {
* @param previouslyApprovedScopes 已经授权过的 scopes
* @param registeredClient 客户端注册信息
*/
record ConsentResponse(
public record ConsentResponse(
Set<String> scopesToApprove,
Set<String> previouslyApprovedScopes,
RegisteredClient registeredClient
) {
}
}
}

View File

@@ -1,41 +0,0 @@
package com.arrokoth.standalone.authorization.service.impl;
import com.arrokoth.standalone.authorization.domain.request.LoginRequest;
import com.arrokoth.standalone.authorization.domain.response.BasicUser;
import com.arrokoth.standalone.authorization.domain.response.Token;
import com.arrokoth.standalone.authorization.service.AuthorizationService;
import com.arrokoth.standalone.authorization.util.JwtUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class AuthorizationServiceImpl implements AuthorizationService {
@Override
public Token login(LoginRequest loginRequest) {
String admin = JwtUtils.createAccessToken("admin");
Token token = new Token();
token.setAccess_token(admin);
token.setExpires_in(JwtUtils.DEFAULT_EXPIRATION);
return token;
}
@Override
public void logout(String jti) {
}
@Override
public BasicUser getBasicUser() {
BasicUser basicUser = new BasicUser();
basicUser.setUsername("admin");
basicUser.setAvatar("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
basicUser.setIntroduction("I am a super administrator...");
basicUser.setRoles(List.of("admin"));
return basicUser;
}
}

View File

@@ -1,71 +0,0 @@
package com.arrokoth.standalone.authorization.service.impl;
import com.arrokoth.standalone.authorization.service.OAuth2ConsentService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.util.*;
/**
* OAuth2 授权确认服务实现类
*/
@Service
@RequiredArgsConstructor
public class OAuth2ConsentServiceImpl implements OAuth2ConsentService {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
/**
* 获取用户授权的详细信息,包括需要审批和已审批的 scopes
*
* @param clientId 客户端 ID
* @param scope 请求的 scope 列表(空格分隔)
* @param principalName 用户名
* @return 包含待批准和已批准 scopes 的 ConsentResult 对象
*/
public ConsentResponse getConsentDetails(Principal principal,
String clientId,
String scope,
String state,
String principalName) {
Set<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
RegisteredClient registeredClient = Optional.ofNullable(
registeredClientRepository.findByClientId(clientId))
.orElseThrow(() -> new IllegalArgumentException("客户端不存在"));
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principalName);
Set<String> authorizedScopes = Optional.ofNullable(currentAuthorizationConsent)
.map(OAuth2AuthorizationConsent::getScopes)
.orElse(Set.of());
Arrays.stream(StringUtils.delimitedListToStringArray(scope, " "))
.filter(requestedScope -> !OidcScopes.OPENID.equals(requestedScope))
.forEach(requestedScope -> {
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
});
return new ConsentResponse(
Collections.unmodifiableSet(scopesToApprove),
Collections.unmodifiableSet(previouslyApprovedScopes),
registeredClient
);
}
}

View File

@@ -10,7 +10,7 @@ public class OAuth2AuthorizationServiceStore {
@Bean
public OAuth2AuthorizationService authorizationService() {
public OAuth2AuthorizationService oAuth2AuthorizationService() {
// 基于db的oauth2认证服务还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
return new InMemoryOAuth2AuthorizationService();
}

View File

@@ -1,6 +1,6 @@
package com.arrokoth.standalone.authorization.store.redis;
import com.arrokoth.standalone.authorization.util.JwtUtils;
import com.arrokoth.basic.util.JwtUtils;
import com.nimbusds.jose.JOSEException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;

View File

@@ -1,183 +0,0 @@
package com.arrokoth.standalone.authorization.util;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.userdetails.UserDetails;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
public class JwtUtils {
// ================== 配置常量 ==================
private static final String ISSUER = "your-issuer";
public static final Long DEFAULT_EXPIRATION = 3600000L; // 默认有效期 1小时 (ms)
private static final String SECRET_KEY = generateSecureSecret();
// ================== 公共方法 ==================
/**
* 创建一个新的访问 Token
*
* @param username 用户标识(如 username 或 user id
* @return JWT Token 字符串
*/
public static String createAccessToken(String username) {
try {
return createAccessToken(username, username, DEFAULT_EXPIRATION);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
/**
* 创建一个新的访问 Token并指定过期时间
*
* @param subject 用户标识
* @param expiration 过期时间(毫秒)
* @return JWT Token 字符串
*/
public static String createAccessToken(String subject, String username, Long expiration) throws JOSEException {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject(subject)
.issuer(ISSUER)
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + expiration))
.jwtID(UUID.randomUUID().toString())
.claim("username", username) // 自定义字段存 username
.build();
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
MACSigner signer = new MACSigner(SECRET_KEY.getBytes());
signedJWT.sign(signer);
return signedJWT.serialize();
}
/**
* 解析并验证 Token
*
* @param token 要解析的 JWT 字符串
* @return JWT 的 Claims
*/
public static JWTClaimsSet parseToken(String token) throws ParseException, JOSEException {
SignedJWT signedJWT = SignedJWT.parse(token);
MACVerifier verifier = new MACVerifier(SECRET_KEY.getBytes());
if (!signedJWT.verify(verifier)) {
throw new JOSEException("Invalid signature");
}
return signedJWT.getJWTClaimsSet();
}
/**
* 提取用户名
*
* @param token JWT Token 字符串
* @return 用户名
*/
public static String extractUsername(String token) throws ParseException, JOSEException {
return parseToken(token).getSubject();
}
/**
* 获取 Token 的过期时间
*
* @param token JWT Token 字符串
* @return 过期时间
*/
public static Date extractExpiration(String token) throws ParseException, JOSEException {
return parseToken(token).getExpirationTime();
}
/**
* 获取 Token 的 jti (JWT ID)
*
* @param token JWT Token 字符串
* @return jti 值
*/
public static String extractJti(String token) throws ParseException, JOSEException {
return parseToken(token).getJWTID();
}
/**
* 判断 Token 是否已过期
*
* @param token JWT Token 字符串
* @return 是否有效
*/
public static boolean isTokenExpired(String token) {
try {
return extractExpiration(token).before(new Date());
} catch (Exception e) {
return true; // 出错也视为过期
}
}
/**
* 验证 Token 是否属于当前用户且未过期
*
* @param token JWT Token 字符串
* @param userDetails 用户信息
* @return 是否有效
*/
public static boolean validateToken(String token, UserDetails userDetails) {
try {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// ================== 私有工具方法 ==================
/**
* 生成安全的密钥Base64 编码的 256 位密钥)
*/
private static String generateSecureSecret() {
byte[] key = new byte[32]; // 256 bits
new SecureRandom().nextBytes(key);
return Base64.getEncoder().encodeToString(key);
}
// 提取 Token 的辅助方法
public static String extractTokenFromHeader(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
// ================== 测试入口 ==================
public static void main(String[] args) {
try {
String token = createAccessToken("user@example.com");
System.out.println("Generated Token: " + token);
JWTClaimsSet claims = parseToken(token);
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration Time: " + claims.getExpirationTime());
System.out.println("Is Valid? " + validateToken(token, null));
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -3,8 +3,15 @@ server:
spring:
aop:
proxy-target-class: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_base_authorization?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: yyds@8848
application:
name: authorization-server-standalone
data:
@@ -28,9 +35,18 @@ arrokoth:
issuer: https://www.arrokoth-info.com
security:
web:
swagger-ui: false
login-page: /login
logout-success-url: /login?logout
permit-urls:
- /home/login
logging:
level:
root: INFO
com.arrokoth: DEBUG
org.springdoc: INFO
org.springframework: INFO
org.springframework.security: DEBUG
org.springframework.security.oauth2: DEBUG