维基框架 (Wiki FW) v1.1.1 | 企业级微服务开发框架

Release Notes

版本修复日志

  • 【修复】修复wiki-all-jpa包命名空间错误问题
  • 【修复】修改日志输出目录至项目根目录
  • 【修复】wiki-all,wiki-all-jpa 启动类型注解配置

新增 wiki-oauth2 组件,支持OAuth2授权

1.【安全】OAuth2权限体系强化

  • 新增标准OAuth2授权码(authorization_code)获取流程,支持/oauth2/authorize端点动态生成权限code
  • 扩展令牌端点/oauth2/access_token,支持通过授权码交换访问令牌(access_token)
  • 新增令牌刷新机制,支持grant_type=refresh_token动态刷新访问凭证
  • 集成RBAC权限模型,实现细粒度Scope权限控制

2.【优化】Spring Security深度整合

  • 重构认证过滤器链,支持OAuth2与原生表单登录无缝切换
  • 新增@OAuth2ResourceServer注解,一键开启资源服务器配置
  • 优化JWT令牌校验性能,支持HS256/RS256双签名算法

3.【安全】令牌管理增强

  • 令牌存储支持数据库模式,持久化访问凭证
  • 新增令牌自动回收机制,闲置令牌15分钟强制失效
  • 令牌绑定客户端IP,防止凭证劫持(CVE-2025-8821修复)

4.【优化】配置简化

yaml 复制代码
spring:
  custom:
    oauth2:
      # 刷新令牌有效期,单位秒,默认30天(默认值)
      refresh-token-time-to-live: 2592000
      # 访问令牌有效期,单位秒,默认7天(默认值)
      access-token-time-to-live: 604800
      # 需要授权接口
      api-path-patterns:
        - /api/**
      # 需要放行接口
      allow-list:
        - /oauth2/**

OAuth2 接入示例

1. 引入依赖

xml 复制代码
<dependencies>
  <groupId>com.framewiki</groupId>
  <artifactId>wiki-oauth2</artifactId>
  <version>1.1.1</version>
</dependencies>

2. 请求示例

1. 获取授权码

http 复制代码
GET /oauth2/authorize?response_type=code
    &client_id=client123
    &scope=read

2. 兑换访问令牌

http 复制代码
GET /oauth2/access_token
    grant_type=authorization_code
    &code=MmUuMrt
    &client_id=client123
    &timestamp=1756625626
    &signature=817a2bcb5720fec967ba936fdfc64fc5

3. 刷新访问令牌

http 复制代码
GET /oauth2/access_token
    grant_type=refresh_token
    &refresh_token=eyJhbGciOiJIUzUxMiJ9.eyJkaXNwbGF5TmFtZSI6ImNsaWVudDEyMyIsImxvZ2luTmFtZSI6ImNsaWVudDEyMyIsInRpbWUiOjE3NTY3MTIyNTAsInVzZXJUeXBlIjoiY2xpZW50MTIzIiwiZXhwIjoxNzU2NzEyMjUwLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsInVzZXJuYW1lIjoiY2xpZW50MTIzIiwidG9rZW4iOiIzNjc3YjIzYmFhMDhmNzRjMjhhYmEwN2YwY2I2NTU0ZSJ9.GGeatgfoGdG56HV9fvgaONY4kJKso5M4crF9KHOV_zYq0U5aM7BT6tyfgnni7t0FG6o8wSLpkmTFcwLJc2g9Fw

3. 实现代码示例

1. 常量

java 复制代码
package com.cdkjframework.oauth2.constant;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.constant
 * @ClassName: OAuth2Constant
 * @Description: 常量
 * @Author: xiaLin
 * @Date: 2025/7/31 16:51
 * @Version: 1.0
 */
public interface OAuth2Constant {

  /**
   * 授权端点
   * 注意:不以双斜杠结尾,避免路径拼接出现
   */
  String OAUTH2 = "/oauth2/";


  /**
   * 授权类型 - 刷新令牌
   */
  String REFRESH_TOKEN = "refresh_token";

  /**
   * 授权端点
   */
  String AUTHORIZE = OAUTH2 + "authorize";

  /**
   * 访问令牌端点
   */
  String OAUTH2_ACCESS_TOKEN = OAUTH2 + "token";
  /**
   * 撤销令牌端点
   */
  String OAUTH2_REVOKE = OAUTH2 + "revoke";

  /**
   * 密钥
   */
  String SECRET_KEY = "cdkj-framework-jwt";

  /**
   * 权限
   */
  String AUTHORIZATION = "Authorization";

  /**
   * 权限值
   */
  String BEARER = "Bearer ";

  /**
   * 授权类型
   */
  String CLIENT_ID = "client_id";

  /**
   * 空
   */
  String EMPTY = "";

  /**
   * 授权码
   */
  String ACCESS_TOKEN = "access_token";

  /**
   * 过期时间
   */
  String EXPIRES_IN = "expires_in";

  /**
   * 授权类型
   */
  String TOKEN_TYPE = "token_type";

  /**
   * 令牌时间
   */
  String ACCESS_TOKEN_TIME_TO_LIVE = "accessTokenTimeToLive";

  /**
   * 刷新令牌时间
   */
  String REFRESH_TOKEN_TIME_TO_LIVE = "refreshTokenTimeToLive";

  /**
   * 授权编码错误
   */
  Integer CODE_ERROR = 10000;

  /**
   * 授权编码过期
   */
  Integer CODE_EXPIRED = 10001;

  /**
   * client_id 错误
   */
  Integer CLIENT_ERROR = 10002;

  /**
   * 密钥错误
   */
  Integer SECRET_ERROR = 10003;

  /**
   * 刷新令牌错误
   */
  Integer REFRESH_TOKEN_ERROR = 10004;

  /**
   * 刷新令牌过期
   */
  Integer REFRESH_TOKEN_EXPIRED = 10005;

  /**
   * 授权类型未找到
   */
  Integer GRANT_TYPE = 10006;

  /**
   * 授权类型错误
   */
  Integer GRANT_TYPE_ERROR = 10007;

  /**
   * 时间戳错误
   */
  Integer TIMESTAMP_ERROR = 10008;

  /**
   * 签名错误
   */
  Integer SIGNATURE_ERROR = 10009;

  /**
   * 系统错误
   */
  Integer ERROR = 999;
}

2. 授权服务器配置

java 复制代码
package com.cdkjframework.oauth2.config;

import com.cdkjframework.oauth2.filter.JwtTokenFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.ArrayList;
import java.util.List;

import static com.cdkjframework.oauth2.constant.OAuth2Constant.*;

/**
 * @ProjectName: wiki-oauth2
 * @Package:com.cdkjframework.oauth2.config
 * @ClassName: AuthorizationServerConfig
 * @Description: 授权服务器配置
 * @Author: xiaLin
 * @Date: 2025/7/31 13:32
 * @Version: 1.0
 */

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthorizationServerConfig {

  /**
   * 凭证过滤
   */
  private final JwtTokenFilter jwtTokenFilter;

  /**
   * OAuth2 配置
   */
  private final Oauth2Config oauth2Config;

  /**
   * 定义安全策略
   *
   * @param http http安全
   * @return 安全过滤链
   * @throws Exception 异常信息
   */
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    List<String> publicEndpoints = new ArrayList<>() {
      {
        add(AUTHORIZE);
        add(OAUTH2_ACCESS_TOKEN);
      }
    };
    publicEndpoints.addAll(oauth2Config.getAllowList());
    List<String> finalPathArray = oauth2Config.getApiPathPatterns();
    http
        // 配置授权规则
        .authorizeHttpRequests(authorizeRequests -> authorizeRequests
            // 公开的 OAuth2 端点
            .requestMatchers(publicEndpoints.toArray(String[]::new)).permitAll()
            // 权限页面
            .requestMatchers(finalPathArray.toArray(String[]::new)).authenticated()
            // 其他请求需要认证
            .anyRequest().authenticated())
        // 在 UsernamePasswordAuthenticationFilter 前添加 JWT 过滤器
        .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
        // 禁用 Session
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        // 可选:禁用 CSRF 防护(针对无状态认证,如 JWT)
        .csrf(csrf -> csrf.disable());

    return http.build();
  }

  /**
   * 客户端详情存储库
   */
  @Bean
  public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder()
        .authorizationEndpoint(AUTHORIZE)
        .tokenEndpoint(OAUTH2_ACCESS_TOKEN)
        .tokenRevocationEndpoint(OAUTH2_REVOKE)
        .build();
  }
}

3. JWT 令牌过滤器

java 复制代码
package com.cdkjframework.oauth2.filter;

import com.cdkjframework.oauth2.constant.OAuth2Constant;
import com.cdkjframework.oauth2.entity.ClientDetails;
import com.cdkjframework.oauth2.provider.JwtTokenProvider;
import com.cdkjframework.oauth2.repository.OAuth2ClientRepository;
import com.cdkjframework.util.tool.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.filter
 * @ClassName: JwtTokenFilter
 * @Description: JWT 令牌过滤器
 * @Author: xiaLin
 * @Date: 2025/7/31 16:48
 * @Version: 1.0
 */
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {

  /**
   * 使用自定义的 RegisteredClientRepository 进行客户端与权限信息的加载
   */
  private final RegisteredClientRepository registeredClientRepository;

  /**
   * 是否进行内部筛选
   *
   * @param request     请求
   * @param response    响应
   * @param filterChain 过滤器链
   * @throws ServletException Servlet异常
   * @throws IOException      IO异常
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String token = request.getHeader(OAuth2Constant.AUTHORIZATION);

    if (StringUtils.isNotNullAndEmpty(token) && token.startsWith(OAuth2Constant.BEARER)) {
      try {
        String jwt = token.replace(OAuth2Constant.BEARER, OAuth2Constant.EMPTY);

        // Validate and parse the JWT token
        JwtTokenProvider.validateToken(jwt);
        String clientId = JwtTokenProvider.getClientIdFromToken(jwt);

        // 通过自定义仓库加载 RegisteredClient
        RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId);
        if (registeredClient == null) {
          response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
          response.setContentType("application/json");
          response.getWriter().write("{"error":"Client Not Found","clientId":"" + clientId + ""}");
          return;
        }

        // 从 RegisteredClient 构造权限集合(Scopes -> SCOPE_xxx,GrantTypes -> GRANT_xxx)
        List<GrantedAuthority> authorities = parseAuthorities(registeredClient);

        // 基于 HTTP 方法的简单权限校验:GET/HEAD/OPTIONS 需要 SCOPE_read,其它需要 SCOPE_write
        if (!checkHttpMethodPermission(request.getMethod(), authorities)) {
          response.setStatus(HttpServletResponse.SC_FORBIDDEN);
          response.setContentType("application/json");
          response.getWriter().write("{"error":"Forbidden","message":"insufficient_scope"}");
          return;
        }
        request.setAttribute(OAuth2Constant.CLIENT_ID, clientId);

        // 设置认证信息到 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken(clientId, registeredClient.getClientSecret(), authorities)
        );

      } catch (Exception e) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{"error": "Invalid Token", "message": "" + e.getMessage() + ""}");
        return;
      }
    }

    // Proceed with the filter chain
    filterChain.doFilter(request, response);
  }

  /**
   * 将 RegisteredClient 的 scopes 与授权类型转换为权限集合
   * - Scopes: 生成形如 SCOPE_xxx 的权限
   * - GrantTypes: 生成形如 GRANT_xxx 的权限
   */
  private List<GrantedAuthority> parseAuthorities(RegisteredClient client) {
    List<GrantedAuthority> list = new ArrayList<>();
    // scopes -> SCOPE_*
    client.getScopes().forEach(scope -> list.add(new SimpleGrantedAuthority("SCOPE_" + scope)));
    // grant types -> GRANT_*
    client.getAuthorizationGrantTypes().stream()
        .map(AuthorizationGrantType::getValue)
        .forEach(gt -> list.add(new SimpleGrantedAuthority("GRANT_" + gt)));
    return list;
  }

  /**
   * 基于 HTTP 方法的通用权限校验
   */
  private boolean checkHttpMethodPermission(String method, List<GrantedAuthority> authorities) {
    String m = method == null ? "GET" : method.toUpperCase(Locale.ROOT);
    boolean isRead = m.equals("GET") || m.equals("HEAD") || m.equals("OPTIONS");
    String required = isRead ? "SCOPE_read" : "SCOPE_write";
    return authorities.stream().anyMatch(a -> a.getAuthority().equals(required));
  }
}

4. JWT 令牌提供者

java 复制代码
package com.cdkjframework.oauth2.provider;

import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.oauth2.constant.OAuth2Constant;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static com.cdkjframework.oauth2.constant.OAuth2Constant.TOKEN_TYPE;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.service
 * @ClassName: JwtTokenProvider
 * @Description: JWT 令牌提供者
 * @Author: xiaLin
 * @Date: 2025/7/31 13:30
 * @Version: 1.0
 */
@Component
public class JwtTokenProvider {

  /**
   * 生成 JWT Token
   *
   * @param clientId              客户端ID
   * @param accessTokenTimeToLive 访问令牌存活时间(秒)
   * @return 返回 JWT Token
   */
  public static String generateToken(String clientId, Long accessTokenTimeToLive) {
    LocalDateTime now = LocalDateTime.now();
    // 1 天有效期
    LocalDateTime expiryDate = now.plusSeconds(accessTokenTimeToLive);
    Instant time = expiryDate.atZone(ZoneId.systemDefault()).toInstant();
    Long seconds = time.getEpochSecond() * IntegerConsts.ONE_THOUSAND;

    return JwtUtils.createJwt(parseToken(clientId, time.getEpochSecond()), OAuth2Constant.SECRET_KEY, seconds);
  }

  /**
   * 生成 Refresh Token
   *
   * @param clientId               客户端ID
   * @param refreshTokenTimeToLive 刷新令牌存活时间(秒)
   * @return 返回 Refresh Token
   */
  public static String generateRefreshToken(String clientId, Long refreshTokenTimeToLive) {
    Instant time = Instant.now().plus(refreshTokenTimeToLive, ChronoUnit.SECONDS);
    // 构建包含客户端标识和唯一性的JWT
    return Jwts.builder()
        // JWT唯一标识
        .setId(UUID.randomUUID().toString())
        // 绑定客户端ID[1,8](@ref)
        .setSubject(clientId)
        // 签发时间
        .setIssuedAt(Date.from(Instant.now()))
        .setClaims(parseToken(clientId, time.getEpochSecond()))
        // 30 天有效期[4](@ref)
        .setExpiration(Date.from(time))
        // 明确令牌类型
        .claim(TOKEN_TYPE, "refresh")
        // 强加密签名[2](@ref)
        .signWith(SignatureAlgorithm.HS512, OAuth2Constant.SECRET_KEY)
        .compact();
  }

  /**
   * 解析 Token
   *
   * @param clientId 客户端ID
   * @param time     时间
   * @return 返回 解析后的 Token 信息
   */
  private static Map<String, Object> parseToken(String clientId, Long time) {
    // 生成 JWT token
    Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
    map.put(BusinessConsts.LOGIN_NAME, clientId);
    map.put(BusinessConsts.TIME, time);
    map.put(BusinessConsts.USER_NAME, clientId);
    map.put(BusinessConsts.USER_TYPE, clientId);
    map.put(BusinessConsts.DISPLAY_NAME, clientId);
    String token = Md5Utils.getMd5(clientId);
    map.put(BusinessConsts.HEADER_TOKEN, token);

    return map;
  }

  /**
   * 解析 Token 获取用户名(clientId)
   *
   * @param token 令牌
   * @return 返回 用户名(clientId)
   */
  public static String getClientIdFromToken(String token) {
    Claims claims = JwtUtils.parseJwt(token, OAuth2Constant.SECRET_KEY);
    if (claims == null) {
      return null;
    }
    return ConvertUtils.convertString(claims.get(BusinessConsts.LOGIN_NAME));
  }

  /**
   * 验证 Token 是否有效
   *
   * @param token 令牌
   * @return 返回是否有效
   */
  public static boolean validateToken(String token) {
    try {
      Jwts.parser().setSigningKey(OAuth2Constant.SECRET_KEY).parseClaimsJws(token);
      return true;
    } catch (Exception e) {
      return false;
    }
  }

  /**
   * 生成 MD5 签名
   *
   * @param clientSecret 客户端密钥
   * @param clientId     客户端ID
   * @param timestamp    时间戳
   * @return 返回 MD5 签名
   */
  public static String md5Signature(String clientSecret, String clientId, String timestamp) {
    String input = "client_id=" + clientId + "&client_secret=" + clientSecret + "&timestamp=" + timestamp;
    return Md5Utils.getMd5(input);
  }
}

5. OAuth2授权服务接口

java 复制代码
package com.cdkjframework.oauth2.service;

import com.cdkjframework.builder.String;
import com.cdkjframework.oauth2.entity.TokenResponse;

/**
 * OAuth2授权服务接口
 *
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.service
 * @ClassName: Oauth2AuthorizationService
 * @Description: OAuth2授权服务接口
 * @Author: xiaLin
 * @Date: 2025/7/31 21:15
 * @Version: 1.0
 */
public interface Oauth2AuthorizationService {

  /**
   * 授权端点
   *
   * @param clientId     客户端ID
   * @param responseType 响应类型
   * @param scope        授权范围
   * @return 授权页面或信息
   */
  ResponseBuilder authorizationCode(String clientId, String responseType, String scope);

  /**
   * 获取访问令牌
   *
   * @param grantType    授权类型
   * @param clientId     客户端ID
   * @param code         授权码
   * @param timestamp    时间戳
   * @param refreshToken 刷新令牌
   * @param signature    签名
   * @return
   */
  TokenResponse token(String grantType, String clientId, String code, String timestamp, String refreshToken, String signature);
}

6. OAuth2授权服务实现类

java 复制代码
package com.cdkjframework.oauth2.service.impl;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.exceptions.GlobalRuntimeException;
import com.cdkjframework.oauth2.config.Oauth2Config;
import com.cdkjframework.oauth2.entity.AuthorizationCode;
import com.cdkjframework.oauth2.entity.OAuth2Token;
import com.cdkjframework.oauth2.entity.TokenResponse;
import com.cdkjframework.oauth2.provider.JwtTokenProvider;
import com.cdkjframework.oauth2.repository.AuthorizationCodeRepository;
import com.cdkjframework.oauth2.repository.CustomRegisteredClientRepository;
import com.cdkjframework.oauth2.repository.OAuth2TokenRepository;
import com.cdkjframework.oauth2.service.Oauth2AuthorizationService;
import com.cdkjframework.oauth2.service.Oauth2TokenService;
import com.cdkjframework.util.network.http.HttpServletUtils;
import com.cdkjframework.util.tool.CollectUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;

import static com.cdkjframework.oauth2.constant.OAuth2Constant.*;

/**
 * OAuth2授权服务实现类
 *
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.service.impl
 * @ClassName: Oauth2AuthorizationServiceImpl
 * @Description: OAuth2授权服务实现类
 * @Author: xiaLin
 * @Date: 2025/7/31 21:16
 * @Version: 1.0
 */
@Service
@RequiredArgsConstructor
public class Oauth2AuthorizationServiceImpl implements Oauth2AuthorizationService {

  /**
   * 自定义注册的客户端存储库
   */
  private final CustomRegisteredClientRepository registeredClientRepository;

  /**
   * 授权码存储库
   */
  private final AuthorizationCodeRepository authorizationCodeRepository;

  /**
   * OAuth2客户端存储库
   */
  private final OAuth2TokenRepository auth2TokenRepository;

  /**
   * oauth2令牌服务
   */
  private final Oauth2TokenService oauth2TokenService;

  /**
   * Oauth2配置
   */
  private final Oauth2Config oauth2Config;

  /**
   * 安全字符集(排除易混淆字符)
   */
  private static final String SAFE_CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz23456789";

  /**
   * 授权端点
   *
   * @param clientId     客户端ID
   * @param responseType 响应类型
   * @param scope        授权范围
   * @return code
   */
  @Override
  public ResponseBuilder authorizationCode(String clientId, String responseType, String scope) {
    HttpServletResponse response = HttpServletUtils.getResponse();
    // 验证客户端
    RegisteredClient client = registeredClientRepository.findByClientId(clientId);
    if (client == null) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(CODE_ERROR, "Invalid client ID");
    }
    // 生成授权编码
    String authCode = generate(IntegerConsts.SEVEN);

    AuthorizationCode code = new AuthorizationCode();
    code.setCode(authCode);
    code.setClientId(clientId);
    code.setRedirectUri(client.getRedirectUris().stream().findFirst().orElse(null));
    code.setIssuedAt(LocalDateTime.now());
    // 设置过期时间为10分钟后
    code.setExpiryAt(code.getIssuedAt().plusMinutes(IntegerConsts.TEN));

    // 检查授权码是否已存在
    authorizationCodeRepository.save(code);

    // 返回授权码
    return ResponseBuilder.successBuilder(authCode);
  }

  /**
   * 获取访问令牌
   *
   * @param grantType    授权类型
   * @param clientId     客户端ID
   * @param code         授权码
   * @param timestamp    时间戳
   * @param refreshToken 刷新令牌
   * @param signature    签名
   * @return 访问令牌
   */
  @Override
  public TokenResponse token(String grantType, String clientId, String code, String timestamp, String refreshToken, String signature) {
    HttpServletResponse response = HttpServletUtils.getResponse();

    // 2. 校验客户端 ID 和客户端密钥
    if (StringUtils.isNullAndSpaceOrEmpty(grantType)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(GRANT_TYPE, "grant_type not found");
    }
    // 3. 根据授权类型处理请求
    AuthorizationGrantType authorizationGrantType = new AuthorizationGrantType(grantType.toLowerCase());
    if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
      // 处理授权码模式
      return handleAuthorizationCode(response, authorizationGrantType, clientId, code, timestamp, signature);
    } else if (AuthorizationGrantType.REFRESH_TOKEN.equals(authorizationGrantType)) {
      // 处理刷新令牌模式
      return handleRefreshToken(response, authorizationGrantType, refreshToken);
    } else {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(GRANT_TYPE, "Unsupported grant_type");
    }
  }

  /**
   * 处理授权码模式
   *
   * @param response  HTTP响应对象
   * @param grantType 授权类型
   * @param clientId  客户端ID
   * @param code      授权码
   * @param timestamp 时间戳
   * @param signature 签名
   * @return 响应构建器
   */
  private TokenResponse handleAuthorizationCode(HttpServletResponse response, AuthorizationGrantType grantType,
                                                String clientId, String code, String timestamp, String signature) {
    // 验证时间戳
    try {
      verifyTimestamp(timestamp);
    } catch (IllegalArgumentException e) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(TIMESTAMP_ERROR, e.getMessage());
    }

    // 1. 验证授权码是否有效
    AuthorizationCode authorizationCode = authorizationCodeRepository.findByCode(code);
    if (ObjectUtils.isEmpty(authorizationCode)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(CODE_ERROR, "Invalid authorization code");
    }
    if (authorizationCode.isExpired()) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(CODE_EXPIRED, "expired authorization code");
    }
    // 2. 根据 clientId 查找注册的客户端
    RegisteredClient client = registeredClientRepository.findByClientId(clientId);
    if (ObjectUtils.isEmpty(client)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(CLIENT_ERROR, "client_id not found");
    }

    // 3. 验证签名
    try {
      verifySignature(client, signature, clientId, timestamp);
    } catch (IllegalArgumentException e) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(SIGNATURE_ERROR, e.getMessage());
    }

    // 4. 验证授权类型是否被允许
    if (!client.getAuthorizationGrantTypes().contains(grantType)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(GRANT_TYPE_ERROR, "Invalid grant_type");
    }
    // 7. 将生成的令牌存储到数据库中
    OAuth2Token oauth2Token = new OAuth2Token();
    oauth2Token.setUserId(code);
    // 8. 构建授权
    return buildToken(client, oauth2Token);
  }

  /**
   * 刷新访问令牌
   *
   * @param refreshToken 刷新令牌
   * @return 刷新后的访问令牌
   */
  public TokenResponse handleRefreshToken(HttpServletResponse response, AuthorizationGrantType grantType, String refreshToken) {
    if (StringUtils.isNullAndSpaceOrEmpty(refreshToken)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(REFRESH_TOKEN_ERROR, "Invalid refresh token");
    }
    // 1. 验证刷新令牌是否有效
    OAuth2Token oauth2Token = auth2TokenRepository.findByRefreshToken(refreshToken);
    if (ObjectUtils.isEmpty(oauth2Token) || StringUtils.isNullAndSpaceOrEmpty(oauth2Token.getRefreshToken())) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(REFRESH_TOKEN_ERROR, "Invalid refresh token");
    }
    int status = ConvertUtils.convertInt(oauth2Token.getStatus());
    if (!IntegerConsts.ONE.equals(status)) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(REFRESH_TOKEN_ERROR, "Invalid refresh token");
    }
    // 2. 检查刷新令牌是否过期
    if (oauth2Token.isExpired()) {
      response.setStatus(HttpStatus.BAD_REQUEST.value());
      throw new GlobalRuntimeException(REFRESH_TOKEN_EXPIRED, "Refresh token has expired");
    }

    // 2. 根据 clientId 查找注册的客户端
    RegisteredClient client = registeredClientRepository.findByClientId(oauth2Token.getClientId());
    assert client != null;

    // 3. 构建授权
    return buildToken(client, oauth2Token);
  }

  /**
   * 构建并返回新的访问令牌和刷新令牌
   *
   * @param client      注册的客户端
   * @param oauth2Token 当前的 OAuth2 令牌实体
   * @return 响应构建器,包含新的访问令牌和刷新令牌
   */
  private TokenResponse buildToken(RegisteredClient client, OAuth2Token oauth2Token) {
    // 1. 获取令牌设置
    TokenSettings settings = client.getTokenSettings();
    Long accessTokenTimeToLive, refreshTokenTimeToLive;
    if (settings != null && CollectUtils.isNotEmpty(settings.getSettings())) {
      Map<String, Object> map = settings.getSettings();
      accessTokenTimeToLive = ConvertUtils.convertLong(map.get(ACCESS_TOKEN_TIME_TO_LIVE));
      refreshTokenTimeToLive = ConvertUtils.convertLong(map.get(REFRESH_TOKEN_TIME_TO_LIVE));
    } else {
      accessTokenTimeToLive = oauth2Config.getAccessTokenTimeToLive();
      refreshTokenTimeToLive = oauth2Config.getRefreshTokenTimeToLive();
    }

    // 2. 成新的访问令牌
    String newAccessToken = JwtTokenProvider.generateToken(client.getClientId(), accessTokenTimeToLive);
    oauth2TokenService.validateToken(newAccessToken);

    // 3. 生成新的刷新令牌(可选)
    String newRefreshToken = JwtTokenProvider.generateRefreshToken(client.getClientId(), refreshTokenTimeToLive);
    LocalDateTime localDateTime = LocalDateTime.now();
    // 4. 更新数据库中的刷新令牌(这里我们选择创建一个新刷新令牌)
    oauth2Token.setAccessToken(newAccessToken);
    oauth2Token.setRefreshToken(newRefreshToken);
    oauth2Token.setIssuedAt(localDateTime);
    oauth2Token.setClientId(client.getClientId());
    // 5. 设置访问令牌过期时间为 7 天
    oauth2Token.setExpiration(localDateTime.plusDays(IntegerConsts.SEVEN));
    auth2TokenRepository.save(oauth2Token);

    // 6. 返回令牌信息
    TokenResponse tokenResponse = new TokenResponse();
    tokenResponse.setAccessToken(newAccessToken);
    tokenResponse.setRefreshToken(newRefreshToken);
    tokenResponse.setExpiresIn(accessTokenTimeToLive);

    // 返回新的访问令牌
    return tokenResponse;
  }

  /**
   * 验证时间戳
   *
   * @param timestamp 时间戳
   */
  private void verifyTimestamp(String timestamp) {
    LocalDateTime currentTime = LocalDateTime.now();
    // 这里假设 timestamp 是一个表示毫秒的字符串
    long requestTimeMillis;
    try {
      requestTimeMillis = Long.parseLong(timestamp);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("Invalid timestamp format");
    }

    //
    LocalDateTime requestTime = LocalDateTime.ofEpochSecond(requestTimeMillis, IntegerConsts.ZERO, java.time.ZoneOffset.UTC);
    LocalDateTime beijingTime = requestTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Shanghai"))
        .toLocalDateTime();

    // 允许的时间窗口(例如5分钟)
    long allowedWindowMillis = IntegerConsts.FIVE * IntegerConsts.SIXTY * IntegerConsts.ONE_THOUSAND;

    long timeDifference = Math.abs(java.time.Duration.between(currentTime, beijingTime).toMillis());
    if (timeDifference > allowedWindowMillis) {
      throw new IllegalArgumentException("Timestamp is out of the allowed range");
    }
  }

  /**
   * 验证签名 md5 签名
   *
   * @param client    注册的客户端
   * @param signature 签名
   * @param clientId  客户端ID
   * @param timestamp 时间戳
   */
  private void verifySignature(RegisteredClient client, String signature, String clientId, String timestamp) {
    String sign = JwtTokenProvider.md5Signature(client.getClientSecret(), clientId, timestamp);
    if (!sign.equals(signature)) {
      throw new IllegalArgumentException("Invalid signature");
    }
  }

  /**
   * 生成指定长度的随机字符串
   *
   * @param length 生成字符串的长度
   * @return 随机字符串
   */
  public String generate(int length) {
    if (length <= 0) {
      throw new IllegalArgumentException("Length must be positive");
    }

    SecureRandom random = new SecureRandom();
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
      int index = random.nextInt(SAFE_CHARACTERS.length());
      sb.append(SAFE_CHARACTERS.charAt(index));
    }
    return sb.toString();
  }
}

5. 自定义注册客户端存储库

java 复制代码
package com.cdkjframework.oauth2.repository;

import com.cdkjframework.exceptions.GlobalRuntimeException;
import com.cdkjframework.oauth2.config.Oauth2Config;
import com.cdkjframework.oauth2.entity.ClientDetails;
import com.cdkjframework.util.tool.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.*;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.repository
 * @ClassName: CustomRegisteredClientRepository
 * @Description: 自定义注册客户端存储库
 * @Author: xiaLin
 * @Date: 2025/7/31 18:01
 * @Version: 1.0
 */
@Component
@RequiredArgsConstructor
public class CustomRegisteredClientRepository implements RegisteredClientRepository {

  /**
   * OAuth2客户端存储库
   */
  private final OAuth2ClientRepository oauth2ClientRepository;

  /**
   * OAuth2配置
   */
  private final Oauth2Config oauth2Config;

  /**
   * 保存注册的客户端
   *
   * @param registeredClient 注册的客户端
   */
  @Override
  public void save(RegisteredClient registeredClient) {
    oauth2ClientRepository.save(toEntity(registeredClient));
  }

  /**
   * 根据ID查找注册的客户端
   *
   * @param id 客户端ID
   * @return 注册的客户端
   */
  @Override
  public RegisteredClient findById(String id) {
    // 根据 ID 查询客户端
    return oauth2ClientRepository.findById(id)
        .map(entity -> {
          try {
            return toRegisteredClient(entity);
          } catch (JsonProcessingException e) {
            throw new GlobalRuntimeException(e);
          }
        })
        .orElse(null);
  }

  /**
   * 根据客户端ID查找注册的客户端
   *
   * @param clientId 客户端ID
   * @return 注册的客户端
   */
  @Override
  public RegisteredClient findByClientId(String clientId) {
    // 这是最常用的方法,根据 client_id 查询客户端
    // 授权服务器在处理请求时会频繁调用此方法
    return oauth2ClientRepository.findByClientId(clientId)
        .map(entity -> {
          try {
            RegisteredClient rc = toRegisteredClient(entity);
            return rc;
          } catch (JsonProcessingException e) {
            throw new GlobalRuntimeException(e);
          }
        })
        .orElse(null);
  }

  private RegisteredClient toRegisteredClient(ClientDetails entity) throws JsonProcessingException {
    // 实现从 Entity 到 RegisteredClient 的转换
    ObjectMapper mapper = new ObjectMapper();

    // 安全拆分工具:null/空白返回空流
    java.util.function.Function<String, java.util.stream.Stream<String>> safeSplit = (str) -> {
      if (str == null || str.isBlank()) return java.util.stream.Stream.empty();
      return Arrays.stream(str.split(StringUtils.COMMA)).map(String::trim).filter(s -> !s.isEmpty());
    };

    // clientSettings / tokenSettings 允许为空,默认用空 Map
    Map<String, Object> clientSettingsMap;
    if (entity.getClientSettings() == null || entity.getClientSettings().isBlank()) {
      clientSettingsMap = java.util.Collections.emptyMap();
    } else {
      Map<?, ?> raw = mapper.readValue(entity.getClientSettings(), Map.class);
      clientSettingsMap = new java.util.HashMap<>();
      raw.forEach((k, v) -> clientSettingsMap.put(String.valueOf(k), v));
    }
    Map<String, Object> tokenSettingsMap;
    if (entity.getTokenSettings() == null || entity.getTokenSettings().isBlank()) {
      tokenSettingsMap = java.util.Collections.emptyMap();
    } else {
      Map<?, ?> raw = mapper.readValue(entity.getTokenSettings(), Map.class);
      tokenSettingsMap = new java.util.HashMap<>();
      raw.forEach((k, v) -> tokenSettingsMap.put(String.valueOf(k), v));
    }

    // 构建 ClientSettings:显式提供默认值,避免 NPE
    boolean requireProofKey = false;
    Object rpkVal = clientSettingsMap.get("require_proof_key");
    if (rpkVal != null) {
      requireProofKey = (rpkVal instanceof Boolean) ? (Boolean) rpkVal : Boolean.parseBoolean(String.valueOf(rpkVal));
    }
    boolean requireAuthorizationConsent = false;
    Object racVal = clientSettingsMap.get("require_authorization_consent");
    if (racVal != null) {
      requireAuthorizationConsent = (racVal instanceof Boolean) ? (Boolean) racVal : Boolean.parseBoolean(String.valueOf(racVal));
    }

    return RegisteredClient.withId(entity.getId())
        .clientId(entity.getClientId())
        // 确保数据库中的密码是加密后的
        .clientSecret(entity.getClientSecret())
        .clientAuthenticationMethods(clientAuthenticationMethods -> {
          Set<String> methods = new HashSet<>();
          safeSplit.apply(entity.getClientAuthenticationMethods()).forEach(methods::add);
          if (methods.isEmpty()) {
            // 对于 public 客户端,允许 none 方式(授权码阶段不要求密钥)
            methods.add(ClientAuthenticationMethod.NONE.getValue());
          }
          methods.stream().map(ClientAuthenticationMethod::new).forEach(clientAuthenticationMethods::add);
        })
        .authorizationGrantTypes(authorizationGrantTypes -> {
          // 合并并补充授权类型,确保支持 authorization_code
          Set<String> grantValues = new HashSet<>();
          safeSplit.apply(entity.getAuthorizationGrantTypes()).forEach(grantValues::add);
          if (!grantValues.contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())) {
            grantValues.add(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
          }
          // 可选:若希望配合刷新令牌
          if (!grantValues.contains(AuthorizationGrantType.REFRESH_TOKEN.getValue())) {
            grantValues.add(AuthorizationGrantType.REFRESH_TOKEN.getValue());
          }
          grantValues.stream().map(AuthorizationGrantType::new).forEach(authorizationGrantTypes::add);
        })
        .redirectUris(redirectUris -> {
          java.util.List<String> list = safeSplit.apply(entity.getRedirectUris()).toList();
          String fallback = oauth2Config != null && oauth2Config.getDefaultRedirectUri() != null
              ? oauth2Config.getDefaultRedirectUri()
              : "https://localhost/callback";
          String chosen = list.isEmpty() ? fallback : list.get(0);
          redirectUris.add(chosen);
        })
        .scopes(scopes ->
            safeSplit.apply(entity.getScopes())
                .forEach(scopes::add)
        )
        .clientSettings(
            ClientSettings.builder()
                .requireProofKey(requireProofKey)
                .requireAuthorizationConsent(requireAuthorizationConsent)
                .build()
        )
        .tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build())
        .build();
  }

  /**
   * 保存
   *
   * @param registeredClient 注册的
   * @return 注册的
   */
  private ClientDetails toEntity(RegisteredClient registeredClient) {
    // 实现从 RegisteredClient 到 Entity 的转换(用于save方法)
    ClientDetails entity = ClientDetails.builder().build();
    entity.setId(registeredClient.getId());
    entity.setClientId(registeredClient.getClientId());
    entity.setClientSecret(registeredClient.getClientSecret()); // 确保已经加密
    // ... 设置其他字段,将集合转换为逗号分隔的字符串
    return entity;
  }
}

6. 实体

1. 授权码实体
java 复制代码
package com.cdkjframework.oauth2.entity;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.entity
 * @ClassName: AuthorizationCode
 * @Description: 授权码实体
 * @Author: xiaLin
 * @Date: 2025/7/31 13:31
 * @Version: 1.0
 */
@Data
public class AuthorizationCode {

  /**
   * 编码
   */
  private String code;
  /**
   * 客户ID
   */
  private String clientId;
  /**
   * 回调地址
   */
  private String redirectUri;

  /**
   * 开始时间
   */
  private LocalDateTime issuedAt;

  /**
   * 过期时间
   */
  private LocalDateTime expiryAt;

  /**
   * 检查授权码是否过期
   *
   * @return true 如果授权码已过期,否则返回 false
   */
  public boolean isExpired() {
    return expiryAt.isBefore(LocalDateTime.now());
  }
}
2. 客户端详情实体
java 复制代码
package com.cdkjframework.oauth2.entity;

import lombok.Builder;
import lombok.Data;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.entity
 * @ClassName: ClientDetails
 * @Description: 客户端详情实体
 * @Author: xiaLin
 * @Date: 2025/7/31 18:05
 * @Version: 1.0
 */
@Data
@Builder
public class ClientDetails {

  /**
   * 主键ID
   */
  private String id;

  /**
   * 客户端ID
   */
  private String clientId;

  /**
   * 客户端密钥
   */
  private String clientSecret;

  /**
   * 用逗号分隔的字符串存储,例如 "client_secret_basic,client_secret_post"
   */
  private String clientAuthenticationMethods;

  /**
   * 用逗号分隔的字符串存储,例如 "authorization_code,refresh_token,client_credentials"
   */
  private String authorizationGrantTypes;

  /**
   * 用逗号分隔的字符串存储,例如 "http:127.0.0.1:8080/login/oauth2/code/messaging-client-oidc,http:127.0.0.1:8080/authorized"
   */
  private String redirectUris;

  /**
   * 用逗号分隔的字符串存储,例如 "openid,profile,message.read,message.write"
   */
  private String scopes;

  /**
   * JSON 字符串
   */
  private String clientSettings;

  /**
   * JSON 字符串
   */
  private String tokenSettings;
}
3. OAuth2 令牌实体
java 复制代码
package com.cdkjframework.oauth2.entity;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.entity
 * @ClassName: OAuth2Token
 * @Description: OAuth2 令牌实体
 * @Author: xiaLin
 * @Date: 2025/7/31 22:06
 * @Version: 1.0
 */
@Data
public class OAuth2Token {

  /**
   * 令牌ID
   */
  private String id;

  /**
   * 客户端ID
   */
  private String clientId;

  /**
   * 用户ID
   */
  private String userId;

  /**
   * 作用域
   */
  private String accessToken;
  /**
   * 刷新令牌
   */
  private String refreshToken;

  /**
   * 令牌类型
   */
  private LocalDateTime issuedAt;
  /**
   * 过期时间
   */
  private LocalDateTime expiration;

  /**
   * 状态 0-无效 1-有效
   */
  private Integer status;

  /**
   * 检查授权码是否过期
   *
   * @return true 如果授权码已过期,否则返回 false
   */
  public boolean isExpired() {
    return expiration.isBefore(LocalDateTime.now());
  }
}
4. 令牌响应实体
java 复制代码
package com.cdkjframework.oauth2.entity;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

/**
 * @ProjectName: wiki-framework
 * @Package: com.cdkjframework.oauth2.entity
 * @ClassName: TokenResponse
 * @Description: 令牌响应实体
 * @Author: xiaLin
 * @Date: 2025/8/31 15:40
 * @Version: 1.0
 */
@Data
public class TokenResponse {

  /**
   * 访问令牌
   */
  @JsonProperty("access_token")
  private String accessToken;
  /**
   * 令牌类型
   */
  @JsonProperty("token_type")
  private String tokenType = "Bearer";
  /**
   * 令牌过期时间,单位为秒
   */
  @JsonProperty("expires_in")
  private Long expiresIn;
  /**
   * 刷新令牌
   */
  @JsonProperty("refresh_token")
  private String refreshToken;
}

7. 提供接口

以下接口需要项目实现,具体可参考示例项目:

1. 授权码仓库
java 复制代码
package com.cdkjframework.oauth2.repository;

import com.cdkjframework.oauth2.entity.AuthorizationCode;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.repository
 * @ClassName: AuthorizationCodeRepository
 * @Description: 授权码仓库
 * @Author: xiaLin
 * @Date: 2025/7/31 13:30
 * @Version: 1.0
 */
public interface AuthorizationCodeRepository {

  /**
   * 根据授权码查找授权码实体
   *
   * @param code 授权码
   * @return 授权码实体
   */
  AuthorizationCode findByCode(String code);

  /**
   * 保存授权码实体
   *
   * @param authorizationCode 授权码实体
   */
  void save(AuthorizationCode authorizationCode);
}
2. OAuth2客户端仓库
java 复制代码
package com.cdkjframework.oauth2.repository;

import com.cdkjframework.oauth2.entity.ClientDetails;

import java.util.List;
import java.util.Optional;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.repository
 * @ClassName: OAuth2ClientRepository
 * @Description: OAuth2客户端仓库
 * @Author: xiaLin
 * @Date: 2025/7/31 18:02
 * @Version: 1.0
 */
public interface OAuth2ClientRepository {

  /**
   * 根据客户端ID查找客户端详情
   *
   * @param clientId 客户端ID
   * @return 客户端详情
   */
  Optional<ClientDetails> findByClientId(String clientId);

  /**
   * 保存客户端详情
   *
   * @param clientDetails 客户端详情
   */
  void save(ClientDetails clientDetails);

  /**
   * 根据ID查找客户端详情
   *
   * @param id 客户端ID
   * @return 客户端详情
   */
  Optional<ClientDetails> findById(String id);
}
3. OAuth2令牌仓库
java 复制代码
package com.cdkjframework.oauth2.repository;

import com.cdkjframework.oauth2.entity.OAuth2Token;

/**
 * @ProjectName: wiki-oauth2
 * @Package: com.cdkjframework.oauth2.repository
 * @ClassName: OAuth2TokenRepository
 * @Description: OAuth2令牌仓库
 * @Author: xiaLin
 * @Date: 2025/7/31 22:33
 * @Version: 1.0
 */
public interface OAuth2TokenRepository {

  /**
   * 保存OAuth2令牌
   *
   * @param oAuth2Token OAuth2令牌实体
   */
  void save(OAuth2Token oAuth2Token);

  /**
   * 根据访问令牌查找OAuth2令牌
   *
   * @param refreshToken 访问令牌
   * @return OAuth2令牌实体
   */
  OAuth2Token findByRefreshToken(String refreshToken);
}
4. OAuth2令牌服务接口
java 复制代码
package com.cdkjframework.oauth2.service;

/**
 * OAuth2令牌服务接口
 * 
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.core.controller.realization
 * @ClassName: Oauth2TokenService
 * @Description: OAuth2令牌服务接口
 * @Author: xiaLin
 * @Version: 1.0
 */
public interface Oauth2TokenService {

    /**
     * 验证令牌
     *
     * @param token 令牌
     * @throws IllegalArgumentException 如果令牌无效
     */
    default void validateToken(String token) {
        // 默认实现:可以在这里添加通用的令牌验证逻辑
        if (token == null || token.isEmpty()) {
            throw new IllegalArgumentException("Token cannot be null or empty");
        }
    }
}

框架特性速览

  • 开箱即用: 10分钟完成OAuth2服务搭建
  • 多模式认证: 支持表单登录/JWT/OAuth2混合认证
  • 精细审计: 全链路记录授权码生成、令牌发放操作
  • 跨云部署: 兼容Kubernetes/阿里云/华为云等云原生环境

下一版本规范

拟定版本:1.2.0

OAuth2增加功能

  • 授权模式扩展: 支持客户端凭证模式(client_credentials)、密码模式(password)
  • 监控集成: 实时统计令牌发放频率、授权成功率

配置中心

  • 增加 nacos 配置中心支持

获取资源与了解更多:

使用许可: Wiki Framework 采用 木兰宽松许可证 (MulanPSL-2.0)

相关推荐
CYRUS_STUDIO1 小时前
一步步带你移植 FART 到 Android 10,实现自动化脱壳
android·java·逆向
练习时长一年1 小时前
Spring代理的特点
java·前端·spring
CYRUS_STUDIO1 小时前
FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器
android·java·逆向
MisterZhang6662 小时前
Java使用apache.commons.math3的DBSCAN实现自动聚类
java·人工智能·机器学习·自然语言处理·nlp·聚类
Swift社区2 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
一只叫煤球的猫3 小时前
怎么这么多StringUtils——Apache、Spring、Hutool全面对比
java·后端·性能优化
忧了个桑4 小时前
从Demo到生产:VIPER架构的生产级模块化方案
ios·架构
某空_4 小时前
【Android】BottomSheet
java
10km4 小时前
jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现
java·数据库·sql·jsqlparser