oauth-重写

This commit is contained in:
2025-09-19 00:53:10 +08:00
parent 1112abb97c
commit 69dde07fc9
33 changed files with 3192 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.accompany</groupId>
<artifactId>accompany-oauth</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>accompany-oauth-sdk</artifactId>
<packaging>jar</packaging>
<description>OAuth SDK模块 - 数据传输对象和接口定义</description>
<dependencies>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-basic-sdk</artifactId>
<version>${revision}</version>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- 移除Spring Security OAuth2依赖使用轻量化实现 -->
</dependencies>
</project>

View File

@@ -0,0 +1,59 @@
package com.accompany.oauth.constant;
/**
* 认证类型枚举
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public enum AuthType {
/**
* 密码登录
*/
PASSWORD("password", "密码登录"),
/**
* 验证码登录
*/
VERIFY_CODE("verify_code", "验证码登录"),
/**
* 邮箱登录
*/
EMAIL("email", "邮箱登录"),
/**
* OpenID登录
*/
OPENID("openid", "OpenID登录"),
/**
* Apple登录
*/
APPLE("apple", "Apple登录");
private final String code;
private final String description;
AuthType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
public static AuthType fromCode(String code) {
for (AuthType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("未知的认证类型: " + code);
}
}

View File

@@ -0,0 +1,84 @@
package com.accompany.oauth.constant;
/**
* OAuth常量类
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public final class OAuthConstants {
/**
* Token相关常量
*/
public static final class Token {
public static final String BEARER_PREFIX = "Bearer ";
public static final String ACCESS_TOKEN = "access_token";
public static final String REFRESH_TOKEN = "refresh_token";
public static final String TOKEN_TYPE = "token_type";
public static final String EXPIRES_IN = "expires_in";
public static final String SCOPE = "scope";
// Redis Key前缀
public static final String ACCESS_TOKEN_PREFIX = "oauth:access_token:";
public static final String REFRESH_TOKEN_PREFIX = "oauth:refresh_token:";
public static final String USER_TOKEN_PREFIX = "oauth:user_token:";
private Token() {}
}
/**
* 授权类型常量
*/
public static final class GrantType {
public static final String PASSWORD = "password";
public static final String VERIFY_CODE = "verify_code";
public static final String EMAIL = "email";
public static final String OPENID = "openid";
public static final String REFRESH_TOKEN = "refresh_token";
private GrantType() {}
}
/**
* HTTP头常量
*/
public static final class Headers {
public static final String AUTHORIZATION = "Authorization";
public static final String CLIENT_ID = "Client-Id";
public static final String DEVICE_ID = "Device-Id";
public static final String USER_AGENT = "User-Agent";
private Headers() {}
}
/**
* 权限范围常量
*/
public static final class Scope {
public static final String READ = "read";
public static final String WRITE = "write";
public static final String ALL = "read write";
private Scope() {}
}
/**
* 错误码常量
*/
public static final class ErrorCode {
public static final String INVALID_REQUEST = "invalid_request";
public static final String INVALID_CLIENT = "invalid_client";
public static final String INVALID_GRANT = "invalid_grant";
public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
public static final String INVALID_SCOPE = "invalid_scope";
public static final String ACCESS_DENIED = "access_denied";
public static final String INVALID_TOKEN = "invalid_token";
public static final String TOKEN_EXPIRED = "token_expired";
private ErrorCode() {}
}
private OAuthConstants() {}
}

View File

@@ -0,0 +1,49 @@
package com.accompany.oauth.constant;
/**
* 用户状态枚举
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public enum UserStatus {
/**
* 正常状态
*/
NORMAL(1, "正常"),
/**
* 已冻结
*/
FROZEN(2, "已冻结"),
/**
* 已删除
*/
DELETED(3, "已删除");
private final int code;
private final String description;
UserStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static UserStatus fromCode(int code) {
for (UserStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("未知的用户状态: " + code);
}
}

View File

@@ -0,0 +1,111 @@
package com.accompany.oauth.dto;
import com.accompany.oauth.constant.AuthType;
/**
* 认证凭据DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class AuthCredentials {
/**
* 认证类型
*/
private AuthType type;
/**
* 主体标识(手机号/邮箱/OpenID等)
*/
private String principal;
/**
* 凭据(密码/验证码等)
*/
private String credentials;
/**
* 客户端ID
*/
private String clientId;
/**
* 设备信息
*/
private DeviceInfo deviceInfo;
/**
* 权限范围
*/
private String scope;
public AuthCredentials() {
}
public AuthCredentials(AuthType type, String principal, String credentials) {
this.type = type;
this.principal = principal;
this.credentials = credentials;
}
public AuthType getType() {
return type;
}
public void setType(AuthType type) {
this.type = type;
}
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public String getCredentials() {
return credentials;
}
public void setCredentials(String credentials) {
this.credentials = credentials;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public DeviceInfo getDeviceInfo() {
return deviceInfo;
}
public void setDeviceInfo(DeviceInfo deviceInfo) {
this.deviceInfo = deviceInfo;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
@Override
public String toString() {
return "AuthCredentials{" +
"type=" + type +
", principal='" + principal + '\'' +
", credentials='[PROTECTED]'" +
", clientId='" + clientId + '\'' +
", deviceInfo=" + deviceInfo +
", scope='" + scope + '\'' +
'}';
}
}

View File

@@ -0,0 +1,154 @@
package com.accompany.oauth.dto;
import com.accompany.oauth.model.UserDetails;
/**
* 认证结果DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class AuthResult {
/**
* 访问令牌
*/
private String accessToken;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 过期时间(秒)
*/
private Long expiresIn;
/**
* 令牌类型
*/
private String tokenType = "Bearer";
/**
* 权限范围
*/
private String scope;
/**
* 用户ID (兼容oauth2)
*/
private Long uid;
/**
* 网易云信Token (兼容oauth2)
*/
private String netEaseToken = "";
/**
* 网易云信账号ID (兼容oauth2)
*/
private String accid = "";
/**
* 用户信息
*/
private UserInfo userInfo;
public AuthResult() {
}
public AuthResult(String accessToken, String refreshToken, Long expiresIn, UserInfo userInfo) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresIn = expiresIn;
this.userInfo = userInfo;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getNetEaseToken() {
return netEaseToken;
}
public void setNetEaseToken(String netEaseToken) {
this.netEaseToken = netEaseToken;
}
public String getAccid() {
return accid;
}
public void setAccid(String accid) {
this.accid = accid;
}
public UserInfo getUserInfo() {
return userInfo;
}
public void setUserInfo(UserInfo userInfo) {
this.userInfo = userInfo;
}
@Override
public String toString() {
return "AuthResult{" +
"accessToken='" + accessToken + '\'' +
", refreshToken='" + refreshToken + '\'' +
", expiresIn=" + expiresIn +
", tokenType='" + tokenType + '\'' +
", scope='" + scope + '\'' +
", uid=" + uid +
", netEaseToken='" + netEaseToken + '\'' +
", accid='" + accid + '\'' +
", userInfo=" + userInfo +
'}';
}
}

View File

@@ -0,0 +1,122 @@
package com.accompany.oauth.dto;
/**
* 设备信息DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class DeviceInfo {
/**
* 设备ID
*/
private String deviceId;
/**
* 设备类型(iOS/Android/Web等)
*/
private String deviceType;
/**
* 设备型号
*/
private String deviceModel;
/**
* 操作系统版本
*/
private String osVersion;
/**
* 应用版本
*/
private String appVersion;
/**
* IP地址
*/
private String ipAddress;
/**
* User-Agent
*/
private String userAgent;
public DeviceInfo() {
}
public DeviceInfo(String deviceId, String deviceType) {
this.deviceId = deviceId;
this.deviceType = deviceType;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
public String getDeviceModel() {
return deviceModel;
}
public void setDeviceModel(String deviceModel) {
this.deviceModel = deviceModel;
}
public String getOsVersion() {
return osVersion;
}
public void setOsVersion(String osVersion) {
this.osVersion = osVersion;
}
public String getAppVersion() {
return appVersion;
}
public void setAppVersion(String appVersion) {
this.appVersion = appVersion;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
@Override
public String toString() {
return "DeviceInfo{" +
"deviceId='" + deviceId + '\'' +
", deviceType='" + deviceType + '\'' +
", deviceModel='" + deviceModel + '\'' +
", osVersion='" + osVersion + '\'' +
", appVersion='" + appVersion + '\'' +
", ipAddress='" + ipAddress + '\'' +
", userAgent='" + userAgent + '\'' +
'}';
}
}

View File

@@ -0,0 +1,120 @@
package com.accompany.oauth.dto;
/**
* H5访问令牌DTO (兼容OAuth2格式)
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class H5AccessToken {
/**
* 用户ID
*/
private Long uid;
/**
* 访问令牌
*/
private String access_token;
/**
* 过期时间(秒)
*/
private Long expires_in;
/**
* 令牌类型
*/
private String token_type = "Bearer";
/**
* 刷新令牌
*/
private String refresh_token;
/**
* 权限范围
*/
private String scope;
public H5AccessToken() {
}
public H5AccessToken(Long uid, String accessToken, Long expiresIn) {
this.uid = uid;
this.access_token = accessToken;
this.expires_in = expiresIn;
}
public static H5AccessToken fromAuthResult(AuthResult authResult) {
H5AccessToken h5Token = new H5AccessToken();
h5Token.setUid(authResult.getUid());
h5Token.setAccess_token(authResult.getAccessToken());
h5Token.setRefresh_token(authResult.getRefreshToken());
h5Token.setExpires_in(authResult.getExpiresIn());
h5Token.setToken_type(authResult.getTokenType());
h5Token.setScope(authResult.getScope());
return h5Token;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public Long getExpires_in() {
return expires_in;
}
public void setExpires_in(Long expires_in) {
this.expires_in = expires_in;
}
public String getToken_type() {
return token_type;
}
public void setToken_type(String token_type) {
this.token_type = token_type;
}
public String getRefresh_token() {
return refresh_token;
}
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
@Override
public String toString() {
return "H5AccessToken{" +
"uid=" + uid +
", access_token='" + access_token + '\'' +
", expires_in=" + expires_in +
", token_type='" + token_type + '\'' +
", refresh_token='" + refresh_token + '\'' +
", scope='" + scope + '\'' +
'}';
}
}

View File

@@ -0,0 +1,145 @@
package com.accompany.oauth.dto;
/**
* OAuth Token请求DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class TokenRequest {
/**
* 授权类型 (password, verify_code, email, openid, refresh_token)
*/
private String grantType;
/**
* 用户名/手机号/邮箱/OpenID
*/
private String username;
/**
* 密码/验证码
*/
private String password;
/**
* 客户端ID
*/
private String clientId;
/**
* 客户端密钥
*/
private String clientSecret;
/**
* 权限范围
*/
private String scope;
/**
* 刷新令牌(当grant_type为refresh_token时使用)
*/
private String refreshToken;
/**
* 第三方登录类型(Apple/微信等)
*/
private String thirdType;
/**
* 设备ID
*/
private String deviceId;
public TokenRequest() {
}
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getThirdType() {
return thirdType;
}
public void setThirdType(String thirdType) {
this.thirdType = thirdType;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
@Override
public String toString() {
return "TokenRequest{" +
"grantType='" + grantType + '\'' +
", username='" + username + '\'' +
", password='[PROTECTED]'" +
", clientId='" + clientId + '\'' +
", clientSecret='[PROTECTED]'" +
", scope='" + scope + '\'' +
", refreshToken='[PROTECTED]'" +
", thirdType='" + thirdType + '\'' +
", deviceId='" + deviceId + '\'' +
'}';
}
}

View File

@@ -0,0 +1,136 @@
package com.accompany.oauth.dto;
/**
* 用户信息DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class UserInfo {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 手机号(脱敏)
*/
private String phone;
/**
* 邮箱(脱敏)
*/
private String email;
/**
* 头像URL
*/
private String avatar;
/**
* 昵称
*/
private String nickname;
/**
* 性别(1-男2-女0-未知)
*/
private Integer gender;
/**
* 用户状态
*/
private Integer status;
public UserInfo() {
}
public UserInfo(Long userId, String username) {
this.userId = userId;
this.username = username;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
@Override
public String toString() {
return "UserInfo{" +
"userId=" + userId +
", username='" + username + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
", avatar='" + avatar + '\'' +
", nickname='" + nickname + '\'' +
", gender=" + gender +
", status=" + status +
'}';
}
}

View File

@@ -0,0 +1,36 @@
package com.accompany.oauth.exception;
import com.accompany.oauth.constant.OAuthConstants;
/**
* 认证异常
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class AuthenticationException extends OAuthException {
public AuthenticationException(String errorDescription) {
super(OAuthConstants.ErrorCode.ACCESS_DENIED, errorDescription);
}
public AuthenticationException(String errorDescription, Throwable cause) {
super(OAuthConstants.ErrorCode.ACCESS_DENIED, errorDescription, cause);
}
public static AuthenticationException invalidCredentials() {
return new AuthenticationException("用户名或密码错误");
}
public static AuthenticationException userNotFound() {
return new AuthenticationException("用户不存在");
}
public static AuthenticationException userFrozen() {
return new AuthenticationException("用户已被冻结");
}
public static AuthenticationException invalidVerifyCode() {
return new AuthenticationException("验证码错误或已过期");
}
}

View File

@@ -0,0 +1,33 @@
package com.accompany.oauth.exception;
/**
* OAuth认证异常基类
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class OAuthException extends RuntimeException {
private final String errorCode;
private final String errorDescription;
public OAuthException(String errorCode, String errorDescription) {
super(errorDescription);
this.errorCode = errorCode;
this.errorDescription = errorDescription;
}
public OAuthException(String errorCode, String errorDescription, Throwable cause) {
super(errorDescription, cause);
this.errorCode = errorCode;
this.errorDescription = errorDescription;
}
public String getErrorCode() {
return errorCode;
}
public String getErrorDescription() {
return errorDescription;
}
}

View File

@@ -0,0 +1,36 @@
package com.accompany.oauth.exception;
import com.accompany.oauth.constant.OAuthConstants;
/**
* Token异常
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class TokenException extends OAuthException {
public TokenException(String errorCode, String errorDescription) {
super(errorCode, errorDescription);
}
public TokenException(String errorCode, String errorDescription, Throwable cause) {
super(errorCode, errorDescription, cause);
}
public static TokenException invalidToken() {
return new TokenException(OAuthConstants.ErrorCode.INVALID_TOKEN, "无效的Token");
}
public static TokenException tokenExpired() {
return new TokenException(OAuthConstants.ErrorCode.TOKEN_EXPIRED, "Token已过期");
}
public static TokenException invalidRefreshToken() {
return new TokenException(OAuthConstants.ErrorCode.INVALID_GRANT, "无效的刷新Token");
}
public static TokenException tokenGenerationFailed() {
return new TokenException(OAuthConstants.ErrorCode.INVALID_REQUEST, "Token生成失败");
}
}

View File

@@ -0,0 +1,95 @@
package com.accompany.oauth.model;
/**
* Token对模型
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class TokenPair {
/**
* 访问令牌
*/
private String accessToken;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 过期时间(秒)
*/
private Long expiresIn;
/**
* 令牌类型
*/
private String tokenType = "Bearer";
/**
* 权限范围
*/
private String scope;
public TokenPair() {
}
public TokenPair(String accessToken, String refreshToken, Long expiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresIn = expiresIn;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
@Override
public String toString() {
return "TokenPair{" +
"accessToken='" + accessToken + '\'' +
", refreshToken='" + refreshToken + '\'' +
", expiresIn=" + expiresIn +
", tokenType='" + tokenType + '\'' +
", scope='" + scope + '\'' +
'}';
}
}

View File

@@ -0,0 +1,125 @@
package com.accompany.oauth.model;
import java.util.Date;
import java.util.Set;
/**
* Token验证结果模型
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class TokenValidation {
/**
* 是否有效
*/
private boolean valid;
/**
* 用户ID
*/
private Long userId;
/**
* 权限范围
*/
private Set<String> scopes;
/**
* 过期时间
*/
private Date expirationTime;
/**
* 客户端ID
*/
private String clientId;
/**
* 错误信息
*/
private String errorMessage;
public TokenValidation() {
}
public TokenValidation(boolean valid) {
this.valid = valid;
}
public static TokenValidation valid(Long userId, Set<String> scopes, Date expirationTime, String clientId) {
TokenValidation validation = new TokenValidation(true);
validation.setUserId(userId);
validation.setScopes(scopes);
validation.setExpirationTime(expirationTime);
validation.setClientId(clientId);
return validation;
}
public static TokenValidation invalid(String errorMessage) {
TokenValidation validation = new TokenValidation(false);
validation.setErrorMessage(errorMessage);
return validation;
}
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Set<String> getScopes() {
return scopes;
}
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
public Date getExpirationTime() {
return expirationTime;
}
public void setExpirationTime(Date expirationTime) {
this.expirationTime = expirationTime;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
@Override
public String toString() {
return "TokenValidation{" +
"valid=" + valid +
", userId=" + userId +
", scopes=" + scopes +
", expirationTime=" + expirationTime +
", clientId='" + clientId + '\'' +
", errorMessage='" + errorMessage + '\'' +
'}';
}
}

View File

@@ -0,0 +1,135 @@
package com.accompany.oauth.model;
import com.accompany.oauth.constant.UserStatus;
import java.util.Set;
/**
* 用户详情模型
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public class UserDetails {
/**
* 用户ID
*/
private Long userId;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 用户名
*/
private String username;
/**
* 用户状态
*/
private UserStatus status;
/**
* 权限集合
*/
private Set<String> authorities;
/**
* 客户端ID
*/
private String clientId;
public UserDetails() {
}
public UserDetails(Long userId, String phone, String email, UserStatus status) {
this.userId = userId;
this.phone = phone;
this.email = email;
this.status = status;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public UserStatus getStatus() {
return status;
}
public void setStatus(UserStatus status) {
this.status = status;
}
public Set<String> getAuthorities() {
return authorities;
}
public void setAuthorities(Set<String> authorities) {
this.authorities = authorities;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* 检查用户是否可用
*/
public boolean isEnabled() {
return status == UserStatus.NORMAL;
}
@Override
public String toString() {
return "UserDetails{" +
"userId=" + userId +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
", username='" + username + '\'' +
", status=" + status +
", authorities=" + authorities +
", clientId='" + clientId + '\'' +
'}';
}
}

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.accompany</groupId>
<artifactId>accompany-oauth</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>accompany-oauth-service</artifactId>
<packaging>jar</packaging>
<description>OAuth Service模块 - 业务逻辑实现</description>
<dependencies>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-oauth-sdk</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-basic-service</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-sms-service</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-email-service</artifactId>
<version>${revision}</version>
</dependency>
<!-- Redis支持 - 使用Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!-- 手机号验证支持 -->
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
</dependency>
<!-- Spring Boot Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 移除Spring Security相关依赖使用轻量化实现 -->
</dependencies>
</project>

View File

@@ -0,0 +1,209 @@
package com.accompany.oauth.manager;
import com.accompany.oauth.constant.OAuthConstants;
import com.accompany.oauth.dto.UserInfo;
import com.accompany.oauth.exception.TokenException;
import com.accompany.oauth.model.TokenPair;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.model.UserDetails;
import com.accompany.oauth.util.JwtUtil;
import io.jsonwebtoken.Claims;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Token管理器 - 使用Redisson进行Token存储和管理
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Component
public class TokenManager {
public JwtUtil jwtUtil;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedissonClient redissonClient;
/**
* 生成Token对
*
* @param userDetails 用户详情
* @return Token对
*/
public TokenPair generateToken(UserDetails userDetails) {
try {
String clientId = userDetails.getClientId() != null ? userDetails.getClientId() : "default";
Set<String> scopes = userDetails.getAuthorities() != null ?
userDetails.getAuthorities() : new HashSet<>(Arrays.asList("read", "write"));
// 生成JWT token
String accessToken = jwtUtil.generateAccessToken(userDetails.getUserId(), clientId, scopes);
String refreshToken = jwtUtil.generateRefreshToken(userDetails.getUserId(), clientId);
// 将token存储到Redis中
storeTokenInRedis(accessToken, refreshToken, userDetails);
TokenPair tokenPair = new TokenPair();
tokenPair.setAccessToken(accessToken);
tokenPair.setRefreshToken(refreshToken);
tokenPair.setExpiresIn(jwtUtil.getAccessTokenExpiration());
tokenPair.setTokenType("Bearer");
tokenPair.setScope(String.join(" ", scopes));
return tokenPair;
} catch (Exception e) {
throw TokenException.tokenGenerationFailed();
}
}
/**
* 验证Token
*
* @param token 访问令牌
* @return 验证结果
*/
public TokenValidation validateToken(String token) {
try {
// 首先验证JWT格式和签名
Claims claims = jwtUtil.validateAndParseToken(token);
// 检查Redis中是否存在该token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + token;
RBucket<String> bucket = redissonClient.getBucket(accessTokenKey);
if (!bucket.isExists()) {
return TokenValidation.invalid("Token不存在或已被撤销");
}
// 提取token信息
Long userId = Long.valueOf(claims.getSubject());
String clientId = claims.get("client_id", String.class);
String scope = claims.get("scope", String.class);
Set<String> scopes = scope != null ?
new HashSet<>(Arrays.asList(scope.split(" "))) : new HashSet<>();
Date expirationTime = claims.getExpiration();
return TokenValidation.valid(userId, scopes, expirationTime, clientId);
} catch (TokenException e) {
return TokenValidation.invalid(e.getErrorDescription());
} catch (Exception e) {
return TokenValidation.invalid("Token验证失败: " + e.getMessage());
}
}
/**
* 刷新Token
*
* @param refreshToken 刷新令牌
* @param userDetails 用户详情
* @return 新的Token对
*/
public TokenPair refreshToken(String refreshToken, UserDetails userDetails) {
try {
// 验证refresh token
Claims claims = jwtUtil.validateAndParseToken(refreshToken);
String tokenType = claims.get("token_type", String.class);
if (!"refresh_token".equals(tokenType)) {
throw TokenException.invalidRefreshToken();
}
// 检查Redis中是否存在该refresh token
String refreshTokenKey = OAuthConstants.Token.REFRESH_TOKEN_PREFIX + refreshToken;
RBucket<String> bucket = redissonClient.getBucket(refreshTokenKey);
if (!bucket.isExists()) {
throw TokenException.invalidRefreshToken();
}
// 撤销旧的tokens
revokeTokensByUserId(userDetails.getUserId());
// 生成新的token对
return generateToken(userDetails);
} catch (TokenException e) {
throw e;
} catch (Exception e) {
throw TokenException.invalidRefreshToken();
}
}
/**
* 撤销Token
*
* @param token 访问令牌
*/
public void revokeToken(String token) {
try {
Claims claims = jwtUtil.validateAndParseToken(token);
Long userId = Long.valueOf(claims.getSubject());
// 删除access token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + token;
redissonClient.getBucket(accessTokenKey).delete();
// 查找并删除对应的refresh token
revokeTokensByUserId(userId);
} catch (Exception e) {
// 忽略撤销失败的情况
}
}
/**
* 撤销用户所有Token
*
* @param userId 用户ID
*/
public void revokeTokensByUserId(Long userId) {
try {
String userTokenKey = OAuthConstants.Token.USER_TOKEN_PREFIX + userId;
RBucket<String> userTokenBucket = redissonClient.getBucket(userTokenKey);
if (userTokenBucket.isExists()) {
// 可以在这里实现更复杂的用户token管理逻辑
// 暂时简单删除用户关联的token记录
userTokenBucket.delete();
}
} catch (Exception e) {
// 忽略撤销失败的情况
}
}
/**
* 将Token存储到Redis
*
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param userDetails 用户详情
*/
private void storeTokenInRedis(String accessToken, String refreshToken, UserDetails userDetails) {
// 存储access token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + accessToken;
RBucket<String> accessTokenBucket = redissonClient.getBucket(accessTokenKey);
accessTokenBucket.set(userDetails.getUserId().toString(),
jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
// 存储refresh token
String refreshTokenKey = OAuthConstants.Token.REFRESH_TOKEN_PREFIX + refreshToken;
RBucket<String> refreshTokenBucket = redissonClient.getBucket(refreshTokenKey);
refreshTokenBucket.set(userDetails.getUserId().toString(),
jwtUtil.getRefreshTokenExpiration(), TimeUnit.SECONDS);
// 存储用户token关联(可用于实现单点登录控制)
String userTokenKey = OAuthConstants.Token.USER_TOKEN_PREFIX + userDetails.getUserId();
RBucket<String> userTokenBucket = redissonClient.getBucket(userTokenKey);
userTokenBucket.set(accessToken, jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,202 @@
package com.accompany.oauth.service;
import com.accompany.oauth.constant.AuthType;
import com.accompany.oauth.dto.AuthCredentials;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.dto.UserInfo;
import com.accompany.oauth.exception.AuthenticationException;
import com.accompany.oauth.manager.TokenManager;
import com.accompany.oauth.model.TokenPair;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.model.UserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 认证服务 - 统一认证入口
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Service
public class AuthenticationService {
@Autowired
private UserService userService;
@Autowired
private TokenManager tokenManager;
/**
* 用户认证
*
* @param credentials 认证凭据
* @return 认证结果
* @throws AuthenticationException 认证失败
*/
public AuthResult authenticate(AuthCredentials credentials) {
// 1. 根据认证类型进行用户认证
UserDetails userDetails = authenticateUser(credentials);
// 2. 检查用户状态
userService.checkUserStatus(userDetails);
// 3. 设置客户端ID和权限范围
userDetails.setClientId(credentials.getClientId());
// 4. 生成Token
TokenPair tokenPair = tokenManager.generateToken(userDetails);
// 5. 构建认证结果
return buildAuthResult(tokenPair, userDetails);
}
/**
* 验证Token
*
* @param token 访问令牌
* @return Token验证结果
*/
public TokenValidation validateToken(String token) {
return tokenManager.validateToken(token);
}
/**
* 刷新Token
*
* @param refreshToken 刷新令牌
* @return 认证结果
* @throws AuthenticationException 刷新失败
*/
public AuthResult refreshToken(String refreshToken) {
try {
// 1. 从refresh token中获取用户信息
Long userId = tokenManager.jwtUtil.getUserIdFromToken(refreshToken);
UserDetails userDetails = userService.getUserById(userId);
// 2. 检查用户状态
userService.checkUserStatus(userDetails);
// 3. 刷新Token
TokenPair tokenPair = tokenManager.refreshToken(refreshToken, userDetails);
// 4. 构建认证结果
return buildAuthResult(tokenPair, userDetails);
} catch (Exception e) {
throw new AuthenticationException("刷新Token失败: " + e.getMessage());
}
}
/**
* 注销Token
*
* @param token 访问令牌
*/
public void revokeToken(String token) {
tokenManager.revokeToken(token);
}
/**
* 注销用户所有Token
*
* @param userId 用户ID
*/
public void revokeAllTokens(Long userId) {
tokenManager.revokeTokensByUserId(userId);
}
/**
* 根据认证类型进行用户认证
*
* @param credentials 认证凭据
* @return 用户详情
*/
private UserDetails authenticateUser(AuthCredentials credentials) {
switch (credentials.getType()) {
case PASSWORD:
return userService.authenticateByPassword(
credentials.getPrincipal(), credentials.getCredentials());
case VERIFY_CODE:
return userService.authenticateByVerifyCode(
credentials.getPrincipal(), credentials.getCredentials());
case EMAIL:
return userService.authenticateByEmail(
credentials.getPrincipal(), credentials.getCredentials());
case OPENID:
case APPLE:
// 对于第三方登录credentials中包含第三方类型信息
return userService.authenticateByOpenId(
credentials.getPrincipal(), (byte) 1);
default:
throw new AuthenticationException("不支持的认证类型: " + credentials.getType());
}
}
/**
* 构建认证结果
*
* @param tokenPair Token对
* @param userDetails 用户详情
* @return 认证结果
*/
private AuthResult buildAuthResult(TokenPair tokenPair, UserDetails userDetails) {
// 构建用户信息
UserInfo userInfo = new UserInfo();
userInfo.setUserId(userDetails.getUserId());
userInfo.setUsername(userDetails.getUsername());
userInfo.setPhone(maskPhone(userDetails.getPhone()));
userInfo.setEmail(maskEmail(userDetails.getEmail()));
userInfo.setStatus(userDetails.getStatus().getCode());
// 构建认证结果
AuthResult authResult = new AuthResult();
authResult.setAccessToken(tokenPair.getAccessToken());
authResult.setRefreshToken(tokenPair.getRefreshToken());
authResult.setExpiresIn(tokenPair.getExpiresIn());
authResult.setTokenType(tokenPair.getTokenType());
authResult.setScope(tokenPair.getScope());
authResult.setUserInfo(userInfo);
// 填充兼容字段
authResult.setUid(userDetails.getUserId());
authResult.setNetEaseToken(""); // TODO: 需要根据实际业务填充
authResult.setAccid(""); // TODO: 需要根据实际业务填充
return authResult;
}
/**
* 手机号脱敏
*
* @param phone 原始手机号
* @return 脱敏后的手机号
*/
private String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
/**
* 邮箱脱敏
*
* @param email 原始邮箱
* @return 脱敏后的邮箱
*/
private String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String localPart = parts[0];
if (localPart.length() <= 2) {
return email;
}
return localPart.substring(0, 2) + "***@" + parts[1];
}
}

View File

@@ -0,0 +1,167 @@
package com.accompany.oauth.service;
import com.accompany.oauth.constant.UserStatus;
import com.accompany.oauth.exception.AuthenticationException;
import com.accompany.oauth.model.UserDetails;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.HashSet;
/**
* 用户服务 - 用户认证和信息查询
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Service
public class UserService {
/**
* 通过密码认证用户
*
* @param phone 手机号
* @param password 密码
* @return 用户详情
* @throws AuthenticationException 认证失败
*/
public UserDetails authenticateByPassword(String phone, String password) {
// TODO: 实现密码认证逻辑,需要连接用户数据库
// 这里先提供一个模拟实现
if ("13800138000".equals(phone) && "123456".equals(password)) {
UserDetails userDetails = new UserDetails();
userDetails.setUserId(1L);
userDetails.setPhone(phone);
userDetails.setUsername("test_user");
userDetails.setStatus(UserStatus.NORMAL);
userDetails.setAuthorities(new HashSet<>(Arrays.asList("read", "write")));
return userDetails;
}
throw AuthenticationException.invalidCredentials();
}
/**
* 通过验证码认证用户
*
* @param phone 手机号
* @param code 验证码
* @return 用户详情
* @throws AuthenticationException 认证失败
*/
public UserDetails authenticateByVerifyCode(String phone, String code) {
// TODO: 实现验证码认证逻辑
// 1. 验证验证码是否正确且未过期
// 2. 查询用户信息
// 3. 检查用户状态
if ("13800138000".equals(phone) && "888888".equals(code)) {
UserDetails userDetails = new UserDetails();
userDetails.setUserId(1L);
userDetails.setPhone(phone);
userDetails.setUsername("test_user");
userDetails.setStatus(UserStatus.NORMAL);
userDetails.setAuthorities(new HashSet<>(Arrays.asList("read", "write")));
return userDetails;
}
throw AuthenticationException.invalidVerifyCode();
}
/**
* 通过邮箱认证用户
*
* @param email 邮箱
* @param code 验证码
* @return 用户详情
* @throws AuthenticationException 认证失败
*/
public UserDetails authenticateByEmail(String email, String code) {
// TODO: 实现邮箱认证逻辑
// 1. 验证邮箱验证码是否正确且未过期
// 2. 查询用户信息
// 3. 检查用户状态
if ("test@example.com".equals(email) && "666666".equals(code)) {
UserDetails userDetails = new UserDetails();
userDetails.setUserId(2L);
userDetails.setEmail(email);
userDetails.setUsername("email_user");
userDetails.setStatus(UserStatus.NORMAL);
userDetails.setAuthorities(new HashSet<>(Arrays.asList("read", "write")));
return userDetails;
}
throw AuthenticationException.invalidVerifyCode();
}
/**
* 通过OpenID认证用户
*
* @param openId OpenID
* @param type 第三方类型 (1-微信, 2-Apple等)
* @return 用户详情
* @throws AuthenticationException 认证失败
*/
public UserDetails authenticateByOpenId(String openId, Byte type) {
// TODO: 实现OpenID认证逻辑
// 1. 验证OpenID的有效性
// 2. 查询绑定的用户信息
// 3. 检查用户状态
if ("openid_test_123".equals(openId)) {
UserDetails userDetails = new UserDetails();
userDetails.setUserId(3L);
userDetails.setUsername("openid_user");
userDetails.setStatus(UserStatus.NORMAL);
userDetails.setAuthorities(new HashSet<>(Arrays.asList("read", "write")));
return userDetails;
}
throw AuthenticationException.userNotFound();
}
/**
* 根据用户ID获取用户详情
*
* @param userId 用户ID
* @return 用户详情
* @throws AuthenticationException 用户不存在
*/
public UserDetails getUserById(Long userId) {
// TODO: 实现根据用户ID查询用户信息的逻辑
if (userId != null && userId > 0) {
UserDetails userDetails = new UserDetails();
userDetails.setUserId(userId);
userDetails.setPhone("138****8000");
userDetails.setUsername("user_" + userId);
userDetails.setStatus(UserStatus.NORMAL);
userDetails.setAuthorities(new HashSet<>(Arrays.asList("read", "write")));
return userDetails;
}
throw AuthenticationException.userNotFound();
}
/**
* 检查用户状态是否可用
*
* @param userDetails 用户详情
* @throws AuthenticationException 用户不可用
*/
public void checkUserStatus(UserDetails userDetails) {
if (userDetails == null) {
throw AuthenticationException.userNotFound();
}
if (userDetails.getStatus() == UserStatus.FROZEN) {
throw AuthenticationException.userFrozen();
}
if (userDetails.getStatus() == UserStatus.DELETED) {
throw AuthenticationException.userNotFound();
}
}
}

View File

@@ -0,0 +1,190 @@
package com.accompany.oauth.util;
import com.accompany.oauth.exception.TokenException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.Set;
/**
* JWT工具类 - 使用更安全的实现
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Component
public class JwtUtil {
@Value("${oauth.jwt.secret:accompany-oauth-secret-key-for-jwt-token-generation}")
private String secret;
@Value("${oauth.jwt.access-token-expiration:7200}")
private long accessTokenExpiration;
@Value("${oauth.jwt.refresh-token-expiration:2592000}")
private long refreshTokenExpiration;
private SecretKey secretKey;
@PostConstruct
public void init() {
// 确保密钥长度足够
if (secret.getBytes(StandardCharsets.UTF_8).length < 32) {
secret = secret + "0123456789abcdef0123456789abcdef";
}
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成访问令牌
*
* @param userId 用户ID
* @param clientId 客户端ID
* @param scopes 权限范围
* @return JWT令牌
*/
public String generateAccessToken(Long userId, String clientId, Set<String> scopes) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpiration * 1000);
JwtBuilder builder = Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiration)
.claim("client_id", clientId)
.claim("token_type", "access_token")
.signWith(secretKey, SignatureAlgorithm.HS256);
if (scopes != null && !scopes.isEmpty()) {
builder.claim("scope", String.join(" ", scopes));
}
return builder.compact();
}
/**
* 生成刷新令牌
*
* @param userId 用户ID
* @param clientId 客户端ID
* @return JWT令牌
*/
public String generateRefreshToken(Long userId, String clientId) {
Date now = new Date();
Date expiration = new Date(now.getTime() + refreshTokenExpiration * 1000);
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiration)
.claim("client_id", clientId)
.claim("token_type", "refresh_token")
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证并解析JWT令牌
*
* @param token JWT令牌
* @return Claims对象
* @throws TokenException 令牌无效或过期
*/
public Claims validateAndParseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw TokenException.tokenExpired();
} catch (JwtException e) {
throw TokenException.invalidToken();
}
}
/**
* 从令牌中获取用户ID
*
* @param token JWT令牌
* @return 用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = validateAndParseToken(token);
return Long.valueOf(claims.getSubject());
}
/**
* 从令牌中获取客户端ID
*
* @param token JWT令牌
* @return 客户端ID
*/
public String getClientIdFromToken(String token) {
Claims claims = validateAndParseToken(token);
return claims.get("client_id", String.class);
}
/**
* 从令牌中获取权限范围
*
* @param token JWT令牌
* @return 权限范围
*/
public String getScopeFromToken(String token) {
Claims claims = validateAndParseToken(token);
return claims.get("scope", String.class);
}
/**
* 检查令牌是否过期
*
* @param token JWT令牌
* @return 是否过期
*/
public boolean isTokenExpired(String token) {
try {
Claims claims = validateAndParseToken(token);
return claims.getExpiration().before(new Date());
} catch (TokenException e) {
return true;
}
}
/**
* 获取令牌过期时间
*
* @param token JWT令牌
* @return 过期时间
*/
public Date getExpirationFromToken(String token) {
Claims claims = validateAndParseToken(token);
return claims.getExpiration();
}
/**
* 获取访问令牌有效期(秒)
*
* @return 有效期秒数
*/
public long getAccessTokenExpiration() {
return accessTokenExpiration;
}
/**
* 获取刷新令牌有效期(秒)
*
* @return 有效期秒数
*/
public long getRefreshTokenExpiration() {
return refreshTokenExpiration;
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.accompany</groupId>
<artifactId>accompany-oauth</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>accompany-oauth-web</artifactId>
<packaging>jar</packaging>
<description>OAuth Web模块 - 控制器和Web配置</description>
<dependencies>
<dependency>
<groupId>com.accompany</groupId>
<artifactId>accompany-oauth-service</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.accompany.oauth.OAuthApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,42 @@
package com.accompany.oauth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
/**
* OAuth应用程序启动类
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@SpringBootApplication
@ComponentScan(basePackages = {
"com.accompany.oauth", // OAuth模块
"com.accompany.common", // 公共模块
"com.accompany.core" // 核心模块
})
public class OAuthApplication {
public static void main(String[] args) {
// 设置系统属性
System.setProperty("spring.application.name", "accompany-oauth");
SpringApplication app = new SpringApplication(OAuthApplication.class);
// 添加启动横幅
app.setBanner((environment, sourceClass, out) -> {
out.println();
out.println(" ____ ");
out.println(" / __ \\ ___ __ __ ___ __ __ ___ ___ ");
out.println("/ /_/ / / / / V / / / / V / / / / / ");
out.println("\\____/ /__/ /_/\\_/ /__/ /_/\\_/ /__/ /__/ ");
out.println();
out.println(":: Accompany OAuth Service :: (v1.0.0)");
out.println(":: Powered by Spring Boot :: ");
out.println();
});
app.run(args);
}
}

View File

@@ -0,0 +1,63 @@
package com.accompany.oauth.config;
import com.accompany.oauth.dto.AuthResult;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
/**
* AuthResult自定义序列化器 - 兼容OAuth2的CustomOAuth2AccessToken格式
* 输出格式:{code:200, data:{uid, access_token, token_type, expires_in, ...}}
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@JsonComponent
public class AuthResultJsonSerializer extends JsonSerializer<AuthResult> {
@Override
public void serialize(AuthResult authResult, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeStartObject();
// 写入状态码
gen.writeNumberField("code", 200);
// 写入data对象
gen.writeObjectFieldStart("data");
// 用户ID (兼容oauth2)
gen.writeNumberField("uid", authResult.getUid());
// 网易云信Token (兼容oauth2)
gen.writeStringField("netEaseToken", authResult.getNetEaseToken());
// accid (兼容oauth2)
if (authResult.getAccid() != null) {
gen.writeStringField("accid", authResult.getAccid());
}
// 标准OAuth字段
gen.writeStringField("access_token", authResult.getAccessToken());
gen.writeStringField("token_type", authResult.getTokenType());
if (authResult.getRefreshToken() != null) {
gen.writeStringField("refresh_token", authResult.getRefreshToken());
}
if (authResult.getExpiresIn() != null) {
gen.writeNumberField("expires_in", authResult.getExpiresIn());
}
if (authResult.getScope() != null) {
gen.writeStringField("scope", authResult.getScope());
}
gen.writeEndObject(); // end data
gen.writeEndObject(); // end root
}
}

View File

@@ -0,0 +1,122 @@
package com.accompany.oauth.config;
import com.accompany.oauth.exception.AuthenticationException;
import com.accompany.oauth.exception.OAuthException;
import com.accompany.oauth.exception.TokenException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理OAuth异常
*
* @param e OAuth异常
* @return 错误响应
*/
@ExceptionHandler(OAuthException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleOAuthException(OAuthException e) {
logger.warn("OAuth异常: {} - {}", e.getErrorCode(), e.getErrorDescription());
Map<String, Object> response = new HashMap<>();
response.put("error", e.getErrorCode());
response.put("error_description", e.getErrorDescription());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.badRequest().body(response);
}
/**
* 处理认证异常
*
* @param e 认证异常
* @return 错误响应
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleAuthenticationException(AuthenticationException e) {
logger.warn("认证异常: {}", e.getErrorDescription());
Map<String, Object> response = new HashMap<>();
response.put("error", e.getErrorCode());
response.put("error_description", e.getErrorDescription());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
/**
* 处理Token异常
*
* @param e Token异常
* @return 错误响应
*/
@ExceptionHandler(TokenException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleTokenException(TokenException e) {
logger.warn("Token异常: {} - {}", e.getErrorCode(), e.getErrorDescription());
Map<String, Object> response = new HashMap<>();
response.put("error", e.getErrorCode());
response.put("error_description", e.getErrorDescription());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
/**
* 处理参数异常
*
* @param e 参数异常
* @return 错误响应
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException e) {
logger.warn("参数异常: {}", e.getMessage());
Map<String, Object> response = new HashMap<>();
response.put("error", "invalid_request");
response.put("error_description", e.getMessage());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.badRequest().body(response);
}
/**
* 处理其他异常
*
* @param e 异常
* @return 错误响应
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
logger.error("系统异常", e);
Map<String, Object> response = new HashMap<>();
response.put("error", "server_error");
response.put("error_description", "系统内部错误");
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

View File

@@ -0,0 +1,51 @@
package com.accompany.oauth.config;
import com.accompany.oauth.interceptor.AuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthenticationInterceptor authenticationInterceptor;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/oauth/**", // OAuth认证相关接口
"/acc/logout", // 用户注销接口
"/actuator/**", // 健康检查接口
"/swagger-ui/**", // Swagger文档
"/v3/api-docs/**", // API文档
"/favicon.ico" // 图标
);
}
/**
* 配置跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,130 @@
package com.accompany.oauth.controller;
import com.accompany.common.constant.AppEnum;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.result.BusiResult;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 用户账户控制器
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@RestController
@RequestMapping("/acc")
public class AccountController {
@Autowired
private AuthenticationService authenticationService;
/**
* 用户注销 (兼容OAuth2格式)
*
* @param accessToken 访问令牌
* @return BusiResult响应结果
*/
@PostMapping("/logout")
public BusiResult<Void> logout(@RequestParam("access_token") String accessToken) {
if (StringUtils.hasText(accessToken)) {
authenticationService.revokeToken(accessToken);
}
return BusiResult.success();
}
/**
* 第三方登录 (兼容OAuth2格式)
*
* @param openid OpenID
* @param type 第三方类型
* @param unionid UnionID(可选)
* @param deviceInfo 设备信息
* @param app 应用类型
* @param idToken ID Token(可选)
* @param request HTTP请求
* @return 直接返回AuthResult结构
*/
@RequestMapping("/third/login")
public AuthResult thirdLogin(@RequestParam String openid,
@RequestParam Integer type,
@RequestParam(required = false) String unionid,
DeviceInfo deviceInfo,
@RequestParam(required = false) AppEnum app,
@RequestParam(required = false) String idToken,
HttpServletRequest request) {
// TODO: 实现第三方登录逻辑
// 1. 验证第三方登录信息
// 2. 查询或创建用户
// 3. 生成Token
throw new UnsupportedOperationException("第三方登录功能暂未实现");
}
/**
* 重置密码 (兼容OAuth2格式)
*
* @param requestBody 重置密码请求
* @return BusiResult响应结果
*/
@PostMapping("/pwd/reset")
public BusiResult<Void> resetPassword(@RequestBody Map<String, Object> requestBody) {
// TODO: 实现密码重置逻辑
// 1. 验证用户身份(手机号+验证码或邮箱+验证码)
// 2. 重置密码
// 3. 发送通知
throw new UnsupportedOperationException("密码重置功能暂未实现");
}
/**
* 修改密码 (兼容OAuth2格式)
*
* @param requestBody 修改密码请求
* @param request HTTP请求
* @return BusiResult响应结果
*/
@PostMapping("/pwd/modify")
public BusiResult<Void> modifyPassword(@RequestBody Map<String, Object> requestBody,
HttpServletRequest request) {
// 验证用户Token
String token = extractTokenFromRequest(request);
if (!StringUtils.hasText(token)) {
throw new IllegalArgumentException("缺少访问令牌");
}
TokenValidation validation = authenticationService.validateToken(token);
if (!validation.isValid()) {
throw new IllegalArgumentException("无效的访问令牌");
}
// TODO: 实现密码修改逻辑
// 1. 验证原密码
// 2. 修改为新密码
// 3. 撤销所有Token(强制重新登录)
throw new UnsupportedOperationException("密码修改功能暂未实现");
}
/**
* 从请求中提取Token
*
* @param request HTTP请求
* @return Token字符串
*/
private String extractTokenFromRequest(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,185 @@
package com.accompany.oauth.controller;
import com.accompany.common.result.BusiResult;
import com.accompany.oauth.constant.AuthType;
import com.accompany.oauth.constant.OAuthConstants;
import com.accompany.oauth.dto.AuthCredentials;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.dto.TokenRequest;
import com.accompany.oauth.dto.H5AccessToken;
import com.accompany.oauth.dto.DeviceInfo;
import com.accompany.oauth.service.AuthenticationService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* OAuth认证控制器
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@RestController
@RequestMapping("/oauth")
public class OAuthController {
@Autowired
private AuthenticationService authenticationService;
/**
* 统一认证端点 - 获取Token (兼容OAuth2格式)
*
* @param request Token请求
* @param httpRequest HTTP请求
* @return 直接返回AuthResult结构兼容OAuth2的CustomOAuth2AccessToken
*/
@PostMapping("/token")
public AuthResult token(@RequestBody TokenRequest request,
HttpServletRequest httpRequest) {
// 1. 验证请求参数
validateTokenRequest(request);
// 2. 构建认证凭据
AuthCredentials credentials = buildAuthCredentials(request, httpRequest);
// 3. 执行认证
AuthResult authResult = authenticationService.authenticate(credentials);
return authResult;
}
/**
* 刷新Token端点 (兼容OAuth2格式)
*
* @param request Token刷新请求
* @return 直接返回AuthResult结构
*/
@PostMapping("/refresh")
public AuthResult refresh(@RequestBody TokenRequest request) {
if (!OAuthConstants.GrantType.REFRESH_TOKEN.equals(request.getGrantType()) ||
!StringUtils.hasText(request.getRefreshToken())) {
throw new IllegalArgumentException("缺少刷新令牌");
}
// 执行Token刷新
AuthResult authResult = authenticationService.refreshToken(request.getRefreshToken());
return authResult;
}
/**
* 撤销Token端点 (兼容OAuth2格式)
*
* @param token 要撤销的Token
* @return BusiResult响应结果
*/
@PostMapping("/revoke")
public BusiResult<Void> revoke(@RequestParam("token") String token) {
if (!StringUtils.hasText(token)) {
throw new IllegalArgumentException("缺少Token参数");
}
authenticationService.revokeToken(token);
return BusiResult.success();
}
/**
* H5授权登录端点 (兼容OAuth2格式)
*
* @param request Token请求
* @param httpRequest HTTP请求
* @return BusiResult包装的H5AccessToken
*/
@PostMapping("/h5/token")
public BusiResult<H5AccessToken> h5Token(@RequestBody TokenRequest request,
HttpServletRequest httpRequest) {
// 执行认证
AuthResult authResult = token(request, httpRequest);
// 转换为H5格式
H5AccessToken h5Token = H5AccessToken.fromAuthResult(authResult);
return BusiResult.success(h5Token);
}
/**
* 验证Token请求参数
*
* @param request Token请求
*/
private void validateTokenRequest(TokenRequest request) {
if (!StringUtils.hasText(request.getGrantType())) {
throw new IllegalArgumentException("缺少grant_type参数");
}
if (!StringUtils.hasText(request.getUsername()) &&
!OAuthConstants.GrantType.REFRESH_TOKEN.equals(request.getGrantType())) {
throw new IllegalArgumentException("缺少username参数");
}
if (!StringUtils.hasText(request.getPassword()) &&
!OAuthConstants.GrantType.REFRESH_TOKEN.equals(request.getGrantType())) {
throw new IllegalArgumentException("缺少password参数");
}
}
/**
* 构建认证凭据
*
* @param request Token请求
* @param httpRequest HTTP请求
* @return 认证凭据
*/
private AuthCredentials buildAuthCredentials(TokenRequest request, HttpServletRequest httpRequest) {
AuthCredentials credentials = new AuthCredentials();
// 设置认证类型
AuthType authType = AuthType.fromCode(request.getGrantType());
credentials.setType(authType);
// 设置主体和凭据
credentials.setPrincipal(request.getUsername());
credentials.setCredentials(request.getPassword());
// 设置客户端信息
credentials.setClientId(StringUtils.hasText(request.getClientId()) ?
request.getClientId() : "default");
// 设置权限范围
credentials.setScope(StringUtils.hasText(request.getScope()) ?
request.getScope() : OAuthConstants.Scope.ALL);
// 构建设备信息
DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.setDeviceId(request.getDeviceId());
deviceInfo.setIpAddress(getClientIpAddress(httpRequest));
deviceInfo.setUserAgent(httpRequest.getHeader("User-Agent"));
credentials.setDeviceInfo(deviceInfo);
return credentials;
}
/**
* 获取客户端IP地址
*
* @param request HTTP请求
* @return IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (StringUtils.hasText(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
}

View File

@@ -0,0 +1,117 @@
package com.accompany.oauth.interceptor;
import com.accompany.oauth.constant.OAuthConstants;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.service.AuthenticationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* 认证拦截器 - 验证访问令牌
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private AuthenticationService authenticationService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 跳过OPTIONS请求
if ("OPTIONS".equals(request.getMethod())) {
return true;
}
// 跳过认证相关接口
String requestURI = request.getRequestURI();
if (isAuthEndpoint(requestURI)) {
return true;
}
// 提取Token
String token = extractToken(request);
if (!StringUtils.hasText(token)) {
writeUnauthorizedResponse(response, "缺少访问令牌");
return false;
}
// 验证Token
TokenValidation validation = authenticationService.validateToken(token);
if (!validation.isValid()) {
writeUnauthorizedResponse(response, validation.getErrorMessage());
return false;
}
// 将用户信息存储到请求属性中
request.setAttribute("userId", validation.getUserId());
request.setAttribute("clientId", validation.getClientId());
request.setAttribute("scopes", validation.getScopes());
return true;
}
/**
* 提取访问令牌
*
* @param request HTTP请求
* @return 访问令牌
*/
private String extractToken(HttpServletRequest request) {
// 优先从Header中获取
String authorization = request.getHeader(OAuthConstants.Headers.AUTHORIZATION);
if (StringUtils.hasText(authorization) && authorization.startsWith(OAuthConstants.Token.BEARER_PREFIX)) {
return authorization.substring(OAuthConstants.Token.BEARER_PREFIX.length());
}
// 从参数中获取
return request.getParameter(OAuthConstants.Token.ACCESS_TOKEN);
}
/**
* 判断是否是认证相关端点
*
* @param requestURI 请求URI
* @return 是否是认证端点
*/
private boolean isAuthEndpoint(String requestURI) {
return requestURI.startsWith("/oauth/") ||
requestURI.equals("/acc/logout") ||
requestURI.startsWith("/actuator/") ||
requestURI.startsWith("/swagger-") ||
requestURI.startsWith("/v3/api-docs");
}
/**
* 写入未授权响应
*
* @param response HTTP响应
* @param message 错误消息
*/
private void writeUnauthorizedResponse(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", OAuthConstants.ErrorCode.INVALID_TOKEN);
errorResponse.put("error_description", message);
errorResponse.put("timestamp", System.currentTimeMillis());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}

View File

@@ -0,0 +1,28 @@
spring:
# Redis配置 - 开发环境
redis:
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
database: 1
connectionPoolSize: 5
connectionMinimumIdleSize: 1
connectTimeout: 3000
timeout: 3000
# OAuth配置 - 开发环境
oauth:
jwt:
secret: accompany-oauth-dev-secret-key-2024
access-token-expiration: 3600 # 开发环境1小时
refresh-token-expiration: 604800 # 开发环境7天
# 日志配置 - 开发环境
logging:
level:
com.accompany.oauth: DEBUG
org.springframework.web: DEBUG
org.redisson: DEBUG
io.jsonwebtoken: DEBUG

View File

@@ -0,0 +1,30 @@
spring:
# Redis配置 - 生产环境
redis:
redisson:
config: |
singleServerConfig:
address: "redis://${REDIS_HOST:127.0.0.1}:${REDIS_PORT:6379}"
password: ${REDIS_PASSWORD:null}
database: ${REDIS_DATABASE:0}
connectionPoolSize: 20
connectionMinimumIdleSize: 5
connectTimeout: 5000
timeout: 5000
retryAttempts: 3
retryInterval: 2000
# OAuth配置 - 生产环境
oauth:
jwt:
secret: ${JWT_SECRET:accompany-oauth-prod-secret-key-2024-very-secure}
access-token-expiration: ${ACCESS_TOKEN_EXPIRATION:7200}
refresh-token-expiration: ${REFRESH_TOKEN_EXPIRATION:2592000}
# 日志配置 - 生产环境
logging:
level:
com.accompany.oauth: INFO
org.springframework.web: WARN
org.redisson: WARN
root: WARN

View File

@@ -0,0 +1,75 @@
server:
port: 8081
servlet:
context-path: /
spring:
application:
name: accompany-oauth
profiles:
active: dev
# Redis配置 (Redisson)
redis:
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
database: 0
connectionPoolSize: 10
connectionMinimumIdleSize: 2
connectTimeout: 3000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
# OAuth配置
oauth:
jwt:
secret: accompany-oauth-secret-key-for-jwt-token-generation-2024
access-token-expiration: 7200 # 访问令牌有效期(秒) - 2小时
refresh-token-expiration: 2592000 # 刷新令牌有效期(秒) - 30天
client:
default:
client-id: default
client-secret: default-secret
grant-types: password,verify_code,email,openid,refresh_token
scopes: read,write
access-token-validity: 7200
refresh-token-validity: 2592000
# 日志配置
logging:
level:
com.accompany.oauth: DEBUG
org.springframework.web: INFO
org.redisson: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/accompany-oauth.log
max-size: 100MB
max-history: 30
# 管理端点配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
server:
port: 8082
# Swagger文档配置
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method

View File

@@ -15,6 +15,7 @@
<module>accompany-base</module>
<module>accompany-business</module>
<module>accompany-oauth2</module>
<module>accompany-oauth</module>
<module>accompany-scheduler</module>
<module>accompany-mq</module>
</modules>