From 69dde07fc9924862c5d28fe1d70a265458fb792d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=A8=E5=BF=97=E6=81=92?= <842328916@qq.com>
Date: Fri, 19 Sep 2025 00:53:10 +0800
Subject: [PATCH] =?UTF-8?q?oauth-=E9=87=8D=E5=86=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
accompany-oauth/accompany-oauth-sdk/pom.xml | 35 +++
.../accompany/oauth/constant/AuthType.java | 59 +++++
.../oauth/constant/OAuthConstants.java | 84 +++++++
.../accompany/oauth/constant/UserStatus.java | 49 ++++
.../accompany/oauth/dto/AuthCredentials.java | 111 ++++++++++
.../com/accompany/oauth/dto/AuthResult.java | 154 +++++++++++++
.../com/accompany/oauth/dto/DeviceInfo.java | 122 ++++++++++
.../accompany/oauth/dto/H5AccessToken.java | 120 ++++++++++
.../com/accompany/oauth/dto/TokenRequest.java | 145 ++++++++++++
.../com/accompany/oauth/dto/UserInfo.java | 136 ++++++++++++
.../exception/AuthenticationException.java | 36 +++
.../oauth/exception/OAuthException.java | 33 +++
.../oauth/exception/TokenException.java | 36 +++
.../com/accompany/oauth/model/TokenPair.java | 95 ++++++++
.../oauth/model/TokenValidation.java | 125 +++++++++++
.../accompany/oauth/model/UserDetails.java | 135 +++++++++++
.../accompany-oauth-service/pom.xml | 60 +++++
.../accompany/oauth/manager/TokenManager.java | 209 ++++++++++++++++++
.../oauth/service/AuthenticationService.java | 202 +++++++++++++++++
.../accompany/oauth/service/UserService.java | 167 ++++++++++++++
.../com/accompany/oauth/util/JwtUtil.java | 190 ++++++++++++++++
accompany-oauth/accompany-oauth-web/pom.xml | 45 ++++
.../com/accompany/oauth/OAuthApplication.java | 42 ++++
.../config/AuthResultJsonSerializer.java | 63 ++++++
.../oauth/config/GlobalExceptionHandler.java | 122 ++++++++++
.../com/accompany/oauth/config/WebConfig.java | 51 +++++
.../oauth/controller/AccountController.java | 130 +++++++++++
.../oauth/controller/OAuthController.java | 185 ++++++++++++++++
.../AuthenticationInterceptor.java | 117 ++++++++++
.../src/main/resources/application-dev.yaml | 28 +++
.../src/main/resources/application-prod.yaml | 30 +++
.../src/main/resources/application.yaml | 75 +++++++
pom.xml | 1 +
33 files changed, 3192 insertions(+)
create mode 100644 accompany-oauth/accompany-oauth-sdk/pom.xml
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/AuthType.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/OAuthConstants.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/UserStatus.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthCredentials.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthResult.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/DeviceInfo.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/H5AccessToken.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/TokenRequest.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/UserInfo.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/AuthenticationException.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/OAuthException.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/TokenException.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenPair.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenValidation.java
create mode 100644 accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/UserDetails.java
create mode 100644 accompany-oauth/accompany-oauth-service/pom.xml
create mode 100644 accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/manager/TokenManager.java
create mode 100644 accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/AuthenticationService.java
create mode 100644 accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/UserService.java
create mode 100644 accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/util/JwtUtil.java
create mode 100644 accompany-oauth/accompany-oauth-web/pom.xml
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/OAuthApplication.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/AuthResultJsonSerializer.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/GlobalExceptionHandler.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/WebConfig.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/controller/AccountController.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/controller/OAuthController.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/interceptor/AuthenticationInterceptor.java
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/resources/application-dev.yaml
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/resources/application-prod.yaml
create mode 100644 accompany-oauth/accompany-oauth-web/src/main/resources/application.yaml
diff --git a/accompany-oauth/accompany-oauth-sdk/pom.xml b/accompany-oauth/accompany-oauth-sdk/pom.xml
new file mode 100644
index 000000000..1387d96df
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+
+ com.accompany
+ accompany-oauth
+ 1.0.0
+
+
+ accompany-oauth-sdk
+ jar
+ OAuth SDK模块 - 数据传输对象和接口定义
+
+
+
+ com.accompany
+ accompany-core
+ ${revision}
+
+
+ com.accompany
+ accompany-basic-sdk
+ ${revision}
+
+
+
+ io.jsonwebtoken
+ jjwt
+
+
+
+
+
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/AuthType.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/AuthType.java
new file mode 100644
index 000000000..9ef1bc855
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/AuthType.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/OAuthConstants.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/OAuthConstants.java
new file mode 100644
index 000000000..8ea6eb1bf
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/OAuthConstants.java
@@ -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() {}
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/UserStatus.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/UserStatus.java
new file mode 100644
index 000000000..23d13accc
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/constant/UserStatus.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthCredentials.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthCredentials.java
new file mode 100644
index 000000000..3cbafb3a4
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthCredentials.java
@@ -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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthResult.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthResult.java
new file mode 100644
index 000000000..854ae6e9e
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/AuthResult.java
@@ -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 +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/DeviceInfo.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/DeviceInfo.java
new file mode 100644
index 000000000..5a618a7c2
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/DeviceInfo.java
@@ -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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/H5AccessToken.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/H5AccessToken.java
new file mode 100644
index 000000000..7b6aad212
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/H5AccessToken.java
@@ -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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/TokenRequest.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/TokenRequest.java
new file mode 100644
index 000000000..979d89773
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/TokenRequest.java
@@ -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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/UserInfo.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/UserInfo.java
new file mode 100644
index 000000000..a8ab944b9
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/dto/UserInfo.java
@@ -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 +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/AuthenticationException.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/AuthenticationException.java
new file mode 100644
index 000000000..079fcd6c2
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/AuthenticationException.java
@@ -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("验证码错误或已过期");
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/OAuthException.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/OAuthException.java
new file mode 100644
index 000000000..62180395b
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/OAuthException.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/TokenException.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/TokenException.java
new file mode 100644
index 000000000..ac9b3900d
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/exception/TokenException.java
@@ -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生成失败");
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenPair.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenPair.java
new file mode 100644
index 000000000..dbb607623
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenPair.java
@@ -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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenValidation.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenValidation.java
new file mode 100644
index 000000000..7d3bfbf5f
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/TokenValidation.java
@@ -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 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 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 getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(Set 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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/UserDetails.java b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/UserDetails.java
new file mode 100644
index 000000000..2cf488875
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-sdk/src/main/java/com/accompany/oauth/model/UserDetails.java
@@ -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 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 getAuthorities() {
+ return authorities;
+ }
+
+ public void setAuthorities(Set 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 + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-service/pom.xml b/accompany-oauth/accompany-oauth-service/pom.xml
new file mode 100644
index 000000000..391cff283
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-service/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ com.accompany
+ accompany-oauth
+ 1.0.0
+
+
+ accompany-oauth-service
+ jar
+ OAuth Service模块 - 业务逻辑实现
+
+
+
+ com.accompany
+ accompany-core
+ ${revision}
+
+
+ com.accompany
+ accompany-oauth-sdk
+ ${revision}
+
+
+ com.accompany
+ accompany-basic-service
+ ${revision}
+
+
+ com.accompany
+ accompany-sms-service
+ ${revision}
+
+
+ com.accompany
+ accompany-email-service
+ ${revision}
+
+
+
+ org.redisson
+ redisson-spring-boot-starter
+
+
+
+ com.googlecode.libphonenumber
+ libphonenumber
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/manager/TokenManager.java b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/manager/TokenManager.java
new file mode 100644
index 000000000..dad45bff7
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/manager/TokenManager.java
@@ -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 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 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 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 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 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 accessTokenBucket = redissonClient.getBucket(accessTokenKey);
+ accessTokenBucket.set(userDetails.getUserId().toString(),
+ jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
+
+ // 存储refresh token
+ String refreshTokenKey = OAuthConstants.Token.REFRESH_TOKEN_PREFIX + refreshToken;
+ RBucket refreshTokenBucket = redissonClient.getBucket(refreshTokenKey);
+ refreshTokenBucket.set(userDetails.getUserId().toString(),
+ jwtUtil.getRefreshTokenExpiration(), TimeUnit.SECONDS);
+
+ // 存储用户token关联(可用于实现单点登录控制)
+ String userTokenKey = OAuthConstants.Token.USER_TOKEN_PREFIX + userDetails.getUserId();
+ RBucket userTokenBucket = redissonClient.getBucket(userTokenKey);
+ userTokenBucket.set(accessToken, jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/AuthenticationService.java b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/AuthenticationService.java
new file mode 100644
index 000000000..cf03ebd44
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/AuthenticationService.java
@@ -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];
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/UserService.java b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/UserService.java
new file mode 100644
index 000000000..a19110b37
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/service/UserService.java
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/util/JwtUtil.java b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/util/JwtUtil.java
new file mode 100644
index 000000000..df31fb274
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-service/src/main/java/com/accompany/oauth/util/JwtUtil.java
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-web/pom.xml b/accompany-oauth/accompany-oauth-web/pom.xml
new file mode 100644
index 000000000..406875321
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-web/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ com.accompany
+ accompany-oauth
+ 1.0.0
+
+
+ accompany-oauth-web
+ jar
+ OAuth Web模块 - 控制器和Web配置
+
+
+
+ com.accompany
+ accompany-oauth-service
+ ${revision}
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ com.accompany.oauth.OAuthApplication
+
+
+
+
+ repackage
+
+
+ exec
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/OAuthApplication.java b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/OAuthApplication.java
new file mode 100644
index 000000000..da09dc120
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/OAuthApplication.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/AuthResultJsonSerializer.java b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/AuthResultJsonSerializer.java
new file mode 100644
index 000000000..e9d266d26
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/AuthResultJsonSerializer.java
@@ -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 {
+
+ @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
+ }
+}
\ No newline at end of file
diff --git a/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/GlobalExceptionHandler.java b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/GlobalExceptionHandler.java
new file mode 100644
index 000000000..15d3f535a
--- /dev/null
+++ b/accompany-oauth/accompany-oauth-web/src/main/java/com/accompany/oauth/config/GlobalExceptionHandler.java
@@ -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