oauth-重写
This commit is contained in:
35
accompany-oauth/accompany-oauth-sdk/pom.xml
Normal file
35
accompany-oauth/accompany-oauth-sdk/pom.xml
Normal 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>
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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() {}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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("验证码错误或已过期");
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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生成失败");
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
60
accompany-oauth/accompany-oauth-service/pom.xml
Normal file
60
accompany-oauth/accompany-oauth-service/pom.xml
Normal 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>
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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];
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
45
accompany-oauth/accompany-oauth-web/pom.xml
Normal file
45
accompany-oauth/accompany-oauth-web/pom.xml
Normal 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>
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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
|
@@ -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
|
Reference in New Issue
Block a user