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
×tamp=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 + "×tamp=" + 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 配置中心支持
获取资源与了解更多:
-
Gitee:gitee.com/cdkjframewo...
-
Github:github.com/cdkjframewo...
使用许可: Wiki Framework 采用 木兰宽松许可证 (MulanPSL-2.0)。