oauth-重写-/oauth/token和第三方登录

This commit is contained in:
2025-09-21 01:21:01 +08:00
parent a762e35456
commit 600c439a43
23 changed files with 920 additions and 1337 deletions

View File

@@ -57,16 +57,6 @@ public class AccountService extends ServiceImpl<AccountMapper, Account> {
return accounts.get(0);
}
public Account getAccount(String phone) {
Account account = getAccountByPhone(phone);
if (account == null) {
if (!CommonUtil.checkValidPhone(phone)) {
account = getAccountByErBanNo(Long.valueOf(phone));
}
}
return account;
}
public Account getAccountByPhone(String phone) {
QueryWrapper<Account> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(Account::getPhone, phone);
@@ -145,7 +135,7 @@ public class AccountService extends ServiceImpl<AccountMapper, Account> {
return accountList;
}
public String refreshAndGetNetEaseToken(Account account) throws Exception {
public String refreshAndGetNetEaseToken(Account account) {
TokenRet tokenRet = netEaseService.refreshToken(account.getUid().toString());
//未注册自动注册云信
if (tokenRet != null && StrUtil.isNotEmpty(tokenRet.getDesc()) &&tokenRet.getDesc().contains("not register")) {
@@ -154,20 +144,6 @@ public class AccountService extends ServiceImpl<AccountMapper, Account> {
return (String) tokenRet.getInfo().get("token");
}
public Account refreshNetEaseTokne(Long uid) throws Exception {
Account account = getById(uid);
String uidStr = String.valueOf(account.getUid());
TokenRet tokenRet = netEaseService.refreshToken(uidStr);
String token = (String) tokenRet.getInfo().get("token");
Account accountUpdate = new Account();
accountUpdate.setUid(account.getUid());
accountUpdate.setNeteaseToken(token);
accountUpdate.setErbanNo(account.getErbanNo());
accountUpdate.setDeviceId(account.getDeviceId());
save(accountUpdate);
return accountUpdate;
}
/**
* 初次设置
*

View File

@@ -0,0 +1,82 @@
package com.accompany.oauth.constant;
import java.util.Arrays;
import java.util.Optional;
/**
* 登录类型
*/
public enum LoginTypeEnum {
/**
* 微信
*/
WECHAT((byte) 1),
/**
* QQ
*/
QQ((byte) 2),
/**
* ID萌声号或手机号
*/
ID((byte) 3),
/**
* 手机号一键登录
*/
PHONE((byte) 4),
/**
* 苹果账户
*/
APPLE((byte) 5),
/**
* 冷启动ticket登录
*/
TICKET((byte) 6),
/**
* 账号密码登录
*/
PASSWORD((byte) 7),
/**
* 谷歌登录
*/
GOOGLE((byte) 8),
/**
* line登录
*/
LINE((byte) 9),
/**
* facebook登录
*/
FACEBOOK((byte) 10),
/**
* 邮箱登录
* */
EMAIL((byte) 11),
;
private final byte value;
LoginTypeEnum(byte value) {
this.value = value;
}
public byte getValue() {
return value;
}
public static LoginTypeEnum get(int value) {
Optional<LoginTypeEnum> result = Arrays.stream(LoginTypeEnum.values()).filter(loginTypeEnum ->
loginTypeEnum.value == value).findAny();
if (result.isPresent()) {
return result.get();
}
throw new IllegalArgumentException("value not match");
}
}

View File

@@ -1,11 +1,14 @@
package com.accompany.oauth.dto;
import lombok.Data;
/**
* 认证结果DTO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Data
public class AuthResult {
/**
@@ -43,152 +46,9 @@ public class AuthResult {
*/
private String netEaseToken = "";
/**
* 网易云信账号ID (兼容oauth2)
*/
private String accid = "";
/**
* 用户Token (兼容oauth2)
*/
private String userToken = "";
/**
* 登录Key (兼容oauth2)
*/
private String loginKey = "";
/**
* JWT ID (兼容oauth2)
*/
private String jti = "";
/**
* 用户信息
*/
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 String getUserToken() {
return userToken;
}
public void setUserToken(String userToken) {
this.userToken = userToken;
}
public String getLoginKey() {
return loginKey;
}
public void setLoginKey(String loginKey) {
this.loginKey = loginKey;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
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 + '\'' +
", userToken='" + userToken + '\'' +
", loginKey='" + loginKey + '\'' +
", jti='" + jti + '\'' +
", userInfo=" + userInfo +
'}';
}
}

View File

@@ -0,0 +1,22 @@
package com.accompany.oauth.dto;
public class DayIpMaxRegisterLimitConfig {
private boolean open;
private long max;
public boolean getOpen() {
return open;
}
public void setOpen(boolean open) {
this.open = open;
}
public long getMax() {
return max;
}
public void setMax(long max) {
this.max = max;
}
}

View File

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

View File

@@ -0,0 +1,10 @@
package com.accompany.oauth.dto;
import lombok.Data;
@Data
public class RepeatedDeviceIpRegisterLimitConfig {
private boolean open;
private int repeatedDeviceNumLimit;
private int repeatedIpNumLimit;
}

View File

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

View File

@@ -1,11 +1,14 @@
package com.accompany.oauth.model;
import lombok.Data;
/**
* Token对模型
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Data
public class TokenPair {
/**
@@ -32,64 +35,5 @@ public class TokenPair {
* 权限范围
*/
private String scope;
public TokenPair() {
}
public TokenPair(String accessToken, String refreshToken, Long expiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresIn = expiresIn;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
@Override
public String toString() {
return "TokenPair{" +
"accessToken='" + accessToken + '\'' +
", refreshToken='" + refreshToken + '\'' +
", expiresIn=" + expiresIn +
", tokenType='" + tokenType + '\'' +
", scope='" + scope + '\'' +
'}';
}
}

View File

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

View File

@@ -1,15 +1,14 @@
package com.accompany.oauth.manager;
import com.accompany.common.redis.RedisKey;
import com.accompany.core.service.common.JedisService;
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 com.accompany.oauth.ticket.RedisTicketStore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -18,7 +17,6 @@ import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Token管理器 - 使用Redisson进行Token存储和管理
@@ -31,48 +29,38 @@ public class TokenManager {
@Autowired
public JwtUtil jwtUtil;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTicketStore redisTicketStore;
/**
* 生成Token对
*
* @param userDetails 用户详情
* @return Token对
*/
public TokenPair generateToken(UserDetails userDetails) {
private JedisService jedisService;
public TokenPair generateToken(Long uid) {
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);
String accessToken = jwtUtil.generateAccessToken(uid);
String refreshToken = jwtUtil.generateRefreshToken(uid);
// 存储access token
jedisService.hwrite(RedisKey.uid_access_token.getKey(), uid.toString(), accessToken);
// 存储用户的access token到ticket store
redisTicketStore.storeAccessToken(userDetails.getUserId(), accessToken, jwtUtil.getAccessTokenExpiration());
jedisService.hwrite(RedisKey.uid_ticket.getKey(), uid.toString(), accessToken);
TokenPair tokenPair = new TokenPair();
tokenPair.setAccessToken(accessToken);
tokenPair.setRefreshToken(refreshToken);
//todo
tokenPair.setExpiresIn(jwtUtil.getAccessTokenExpiration());
tokenPair.setTokenType("Bearer");
tokenPair.setScope(String.join(" ", scopes));
tokenPair.setScope(String.join(" ", "read", "write"));
return tokenPair;
} catch (Exception e) {
throw TokenException.tokenGenerationFailed();
}
}
/**
* 验证Token
*
@@ -108,43 +96,6 @@ public class TokenManager {
}
}
/**
* 刷新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
*
@@ -153,62 +104,16 @@ public class TokenManager {
public void revokeToken(String token) {
try {
Claims claims = jwtUtil.validateAndParseToken(token);
Long userId = Long.valueOf(claims.getSubject());
Long uid = Long.valueOf(claims.getSubject());
// 删除access token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + token;
redissonClient.getBucket(accessTokenKey).delete();
// 查找并删除对应的refresh token
revokeTokensByUserId(userId);
jedisService.hdel(RedisKey.uid_access_token.getKey(), uid.toString());
// ticket
jedisService.hdel(RedisKey.uid_ticket.getKey(), uid.toString());
} catch (Exception e) {
// 忽略撤销失败的情况
}
}
/**
* 撤销用户所有Token
*
* @param userId 用户ID
*/
public void revokeTokensByUserId(Long userId) {
try {
String userTokenKey = OAuthConstants.Token.USER_TOKEN_PREFIX + userId;
RBucket<String> userTokenBucket = redissonClient.getBucket(userTokenKey);
if (userTokenBucket.isExists()) {
// 可以在这里实现更复杂的用户token管理逻辑
// 暂时简单删除用户关联的token记录
userTokenBucket.delete();
}
} catch (Exception e) {
// 忽略撤销失败的情况
}
}
/**
* 将Token存储到Redis
*
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param userDetails 用户详情
*/
private void storeTokenInRedis(String accessToken, String refreshToken, UserDetails userDetails) {
// 存储access token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + accessToken;
RBucket<String> accessTokenBucket = redissonClient.getBucket(accessTokenKey);
accessTokenBucket.set(userDetails.getUserId().toString(),
jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
// 存储refresh token
String refreshTokenKey = OAuthConstants.Token.REFRESH_TOKEN_PREFIX + refreshToken;
RBucket<String> refreshTokenBucket = redissonClient.getBucket(refreshTokenKey);
refreshTokenBucket.set(userDetails.getUserId().toString(),
jwtUtil.getRefreshTokenExpiration(), TimeUnit.SECONDS);
// 存储用户token关联(可用于实现单点登录控制)
String userTokenKey = OAuthConstants.Token.USER_TOKEN_PREFIX + userDetails.getUserId();
RBucket<String> userTokenBucket = redissonClient.getBucket(userTokenKey);
userTokenBucket.set(accessToken, jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
}
}

View File

@@ -2,24 +2,37 @@ package com.accompany.oauth.service;
import cn.hutool.core.util.ObjectUtil;
import com.accompany.common.constant.Constant;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.netease.neteaseacc.result.TokenRet;
import com.accompany.common.redis.RedisKey;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.BlankUtil;
import com.accompany.common.utils.CommonUtil;
import com.accompany.common.utils.DateTimeUtil;
import com.accompany.common.utils.UUIDUtil;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.model.Account;
import com.accompany.core.model.GoogleOpenidRef;
import com.accompany.core.model.UserCancelRecord;
import com.accompany.core.model.Users;
import com.accompany.core.mybatismapper.AccountMapper;
import com.accompany.core.service.SysConfService;
import com.accompany.core.service.account.AccountService;
import com.accompany.core.service.account.ErBanNoService;
import com.accompany.core.service.account.NetEaseService;
import com.accompany.core.service.common.JedisService;
import com.accompany.core.service.user.GoogleOpenidRefService;
import com.accompany.core.service.user.UserCancelRecordService;
import com.accompany.core.service.user.UsersBaseService;
import com.accompany.core.util.MD5;
import com.accompany.email.service.EmailService;
import com.accompany.oauth.constant.LoginTypeEnum;
import com.accompany.oauth.dto.DayIpMaxRegisterLimitConfig;
import com.accompany.oauth.dto.RepeatedDeviceIpRegisterLimitConfig;
import com.accompany.sms.service.SmsService;
import com.alibaba.fastjson.JSON;
import com.google.gson.Gson;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -40,6 +53,10 @@ public class AccountManageService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private NetEaseService netEaseService;
@Autowired
private ErBanNoService erBanNoService;
@Autowired
private AccountService accountService;
@Autowired
private UsersBaseService usersBaseService;
@@ -51,9 +68,273 @@ public class AccountManageService {
private SmsService smsService;
@Autowired
private EmailService emailService;
@Autowired
private GoogleOpenidRefService googleOpenidRefService;
protected Gson gson = new Gson();
public Account getAccountPyUsername(String username, String password) {
log.info("getAccountByUserName username:{} password:{}", username, password);
Long erbanNo = Long.parseLong(username);
return accountService.getAccountByErBanNo(erbanNo);
}
public Account getOrGenAccountByPhone(String phone, String phoneAreaCode, String smsCode, DeviceInfo deviceInfo) {
log.info("getOrGenAccountByPhone phone:{},smsCode:{},phoneAreaCode{}", phone, smsCode, phoneAreaCode);
final String lockVal = jedisService.lock(RedisKey.lock_register_by_phone.getKey(phone));
try {
if (BlankUtil.isBlank(lockVal)) {
throw new ServiceException(BusiStatus.REQUEST_FAST);
}
Account account = accountService.getAccountByPhone(phone);
if (account == null) {
account = saveSignUpByPhone(phone, phoneAreaCode, deviceInfo);
}
return account;
} finally {
jedisService.unlock(RedisKey.lock_register_by_phone.getKey(phone), lockVal);
}
}
public Account getOrGenAccountByEmail(String email, String code, DeviceInfo deviceInfo, String ipAddress) {
log.info("getOrGenAccountByPhone email:{},code:{}", email, code);
String lockVal = jedisService.lock(RedisKey.lock_register_by_email.getKey(email));
try {
if (BlankUtil.isBlank(lockVal)) {
throw new ServiceException(BusiStatus.REQUEST_FAST);
}
Account account = accountService.getAccountByEmail(email);
if (account == null) {
account = saveSignUpByEmail(email, null, deviceInfo, ipAddress);
}
return account;
} finally {
jedisService.unlock(RedisKey.lock_register_by_email.getKey(email), lockVal);
}
}
@SneakyThrows
public Account getOrGenAccountByOpenid(Byte type, String openid, String unionId, String idToken, DeviceInfo deviceInfo) {
log.info("getOrGenAccountByOpenid openId:{},type:{},unionId:{}", openid, type, unionId);
final String locKey = RedisKey.lock_register_by_openid.getKey(openid, unionId, String.valueOf(type));
final String lockVal = jedisService.lock(locKey, 10 * 1000);
try {
if (BlankUtil.isBlank(lockVal)) {
throw new ServiceException(BusiStatus.REQUEST_FAST);
}
Account account = null;
String thirdAccountEmail = null;
// openid是邮箱则是新版本
if (LoginTypeEnum.GOOGLE.getValue() == type && emailService.isValidEmail(openid)) {
GoogleOpenidRef ref = googleOpenidRefService.getRefByEmail(openid, idToken);
if (null != ref) {
openid = ref.getOpenId();
unionId = ref.getOpenId();
thirdAccountEmail = ref.getEmail();
if (ref.isRegister()){
Account emailAccount = accountService.getAccountByEmail(thirdAccountEmail);
if (null != emailAccount && null == emailAccount.getThirdLoginType()){
emailAccount.setThirdLoginType(type);
emailAccount.setUnionId(unionId);
emailAccount.setOpenId(openid);
accountMapper.updateById(emailAccount);
account = emailAccount;
}
}
}
}
if (null == account){
account = accountService.getAccountByThird(type, unionId);
}
if (null != account){
if (BlankUtil.isBlank(account.getUnionId()) || !account.getUnionId().equals(unionId)) {
account.setUnionId(unionId);
}
account.setLastLoginTime(new Date());
account.setLastLoginIp(deviceInfo.getClientIp());
account.setUpdateTime(new Date());
accountMapper.updateById(account);
return account;
}
checkRegisterLimit(deviceInfo.getDeviceId(), deviceInfo.getClientIp());
Date date = new Date();
account = new Account();
account.setLastLoginTime(date);
account.setLastLoginIp(deviceInfo.getClientIp());
account.setUpdateTime(date);
account.setRegisterIp(deviceInfo.getClientIp());
account.setSignTime(date);
account.setState(Constant.AccountState.normal);
account.setErbanNo(erBanNoService.getErBanNo());
account.setPhone(CommonUtil.genSpecialPhoneForInitAccount(account.getErbanNo().toString()));
// 三方登录信息
account.setThirdLoginType(type);
account.setUnionId(unionId);
account.setOpenId(openid);
account.setEmail(thirdAccountEmail);
account.setNeteaseToken(UUIDUtil.get());
account = fillDeviceInfo(account, deviceInfo);
accountMapper.insert(account);
//写缓存
accountService.writeAche(account);
String uidStr = String.valueOf(account.getUid());
TokenRet tokenRet = netEaseService.createNetEaseAcc(uidStr, account.getNeteaseToken(), "", "", null);
if (tokenRet.getCode() != 200) {
log.info("注册云信账号失败,openid=" + openid + "&uid=" + uidStr + ",异常原因code=" + tokenRet.getCode());
log.error("注册云信账号失败,openid=" + openid + "&uid=" + uidStr + ",异常原因code=" + tokenRet.getCode());
throw new ServiceException("第三方登录失败,openid=" + openid + ",异常原因code=" + tokenRet.getCode());
}
return account;
} finally {
jedisService.unlock(locKey, lockVal);
}
}
private void checkRegisterLimit(String deviceId, String ipAddress) {
if (!StringUtils.hasText(deviceId)) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.SIGN_IP_TO_OFTEN, BusiStatus.DEVICE_ERROR.getReasonPhrase());
}
RepeatedDeviceIpRegisterLimitConfig repeatedConfig = getRepeatedDeviceIpLimitConfig();
if (repeatedConfig.isOpen()) {
long repeatedDeviceNum = accountService.lambdaQuery().eq(Account::getDeviceId, deviceId).count();
if (repeatedDeviceNum >= repeatedConfig.getRepeatedDeviceNumLimit()) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.SIGN_IP_TO_OFTEN, BusiStatus.REGISTER_FREQUENT.getReasonPhrase());
}
long repeatedIpNum = accountService.lambdaQuery().eq(Account::getRegisterIp, ipAddress).count();
if (repeatedIpNum >= repeatedConfig.getRepeatedIpNumLimit()) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.SIGN_IP_TO_OFTEN, BusiStatus.REGISTER_FREQUENT.getReasonPhrase());
}
}
//当日单个ip注册数
DayIpMaxRegisterLimitConfig config = getIpMaxLimitConfig();
if (config.getOpen()) {
long count = accountService.getRegisterIpCountByOneDay(ipAddress);
if (count >= config.getMax()) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.SIGN_IP_TO_OFTEN, BusiStatus.REGISTER_FREQUENT.getReasonPhrase());
}
}
}
private String encryptPassword(String password) {
return MD5.getMD5(password);
}
/**
* 通过手机号码注册,独立账号系统,不掺杂业务
*
* @param phone
* @return
*/
@SneakyThrows
public Account saveSignUpByPhone(String phone, String phoneAreaCode, DeviceInfo deviceInfo) {
checkRegisterLimit(deviceInfo.getDeviceId(), deviceInfo.getClientIp());
Date date = new Date();
Account account = new Account();
account.setPhone(phone);
account.setPhoneAreaCode(phoneAreaCode);
// if (!StringUtils.isEmpty(password)) {
// account.setPassword(encryptPassword(password));
// }
account.setNeteaseToken(UUIDUtil.get());
account.setLastLoginTime(date);
account.setLastLoginIp(deviceInfo.getClientIp());
account.setUpdateTime(date);
account.setRegisterIp(deviceInfo.getClientIp());
account.setSignTime(date);
account.setState(Constant.AccountState.normal);
account.setErbanNo(erBanNoService.getErBanNo());
account = fillDeviceInfo(account, deviceInfo);
account.setSignupApp(deviceInfo.getApp());
accountMapper.insert(account);
accountService.writeAche(account);
String uidStr = String.valueOf(account.getUid());
TokenRet tokenRet = netEaseService.createNetEaseAcc(uidStr, account.getNeteaseToken(), "", "", null);
if (tokenRet.getCode() != 200) {
log.error("手机号码phone=" + phone + "注册异常,异常原因code=" + tokenRet.getCode());
log.info("手机号码phone=" + phone + "注册异常,异常原因code=" + tokenRet.getCode());
throw new ServiceException("手机号码phone=" + phone + "注册异常,异常原因code=" + tokenRet.getCode());
}
return account;
}
@SneakyThrows
private Account saveSignUpByEmail(String email, String password, DeviceInfo deviceInfo, String ipAddress) {
checkRegisterLimit(deviceInfo.getDeviceId(), ipAddress);
Date date = new Date();
Account account = new Account();
account.setEmail(email);
if (StringUtils.hasText(password)) {
account.setPassword(encryptPassword(password));
}
account.setNeteaseToken(UUIDUtil.get());
account.setLastLoginTime(date);
account.setLastLoginIp(ipAddress);
account.setUpdateTime(date);
account.setRegisterIp(ipAddress);
account.setSignTime(date);
account.setState(Constant.AccountState.normal);
account.setErbanNo(erBanNoService.getErBanNo());
account = fillDeviceInfo(account, deviceInfo);
accountMapper.insert(account);
accountService.writeAche(account);
String uidStr = String.valueOf(account.getUid());
TokenRet tokenRet = netEaseService.createNetEaseAcc(uidStr, account.getNeteaseToken(), "", "", null);
if (tokenRet.getCode() != 200) {
log.error("邮件email {} 注册异常,异常原因code {}", email, tokenRet.getCode());
throw new ServiceException(BusiStatus.SERVERBUSY);
}
return account;
}
private Account fillDeviceInfo(Account account, DeviceInfo deviceInfo) {
if (deviceInfo != null) {
account.setSignupApp(deviceInfo.getApp());
account.setApp(deviceInfo.getApp());
account.setAppVersion(deviceInfo.getAppVersion());
account.setChannel(deviceInfo.getChannel());
account.setLinkedmeChannel(deviceInfo.getLinkedmeChannel());
account.setDeviceId(deviceInfo.getDeviceId());
account.setImei(deviceInfo.getImei());
account.setIspType(deviceInfo.getIspType());
account.setModel(deviceInfo.getModel());
account.setNetType(deviceInfo.getNetType());
account.setOs(deviceInfo.getOs());
account.setOsversion(deviceInfo.getOsVersion());
account.setDeviceInfo(JSON.toJSONString(deviceInfo));
}
return account;
}
/**
* 重置密码
* 两个场景调用 => 客户端未登录 忘记密码, 此时uid 为 null 登录状态下忘记密码 uid有值
@@ -159,10 +440,6 @@ public class AccountManageService {
30 * 24 * 60 * 60, String.valueOf(new Date().getTime()));
}
private String encryptPassword(String password) {
return MD5.getMD5(password);
}
/**
* 重置登录密码
* @param uid
@@ -183,10 +460,57 @@ public class AccountManageService {
// 更新用户缓存
this.jedisService.hdel(RedisKey.user.getKey(), account.getUid().toString());
this.jedisService.hdel(RedisKey.user_summary.getKey(), account.getUid().toString());
accountService.delNickPasswordCache(account.getErbanNo());
}
public void checkAccountCancel(Long uid) {
log.info("检查账号{}是否已注销", uid);
Users users = usersBaseService.getUsersByUid(uid);
if (ObjectUtil.isNull(users)) {
log.info("获取不到用户{}users账号信息", uid);
return;
}
if (!Constant.UserUseStatus.cancel.equals(users.getUseStatus())) return;
UserCancelRecord userCancelRecord = userCancelRecordService.getById(uid);
if (ObjectUtil.isNull(userCancelRecord)) {
//获取不到注销账号信息
log.info("获取不到用户{}注销信息", uid);
throw new CustomOAuth2Exception(CustomOAuth2Exception.ACCOUNT_CANCEL_INFO_NOT_EXIST, BusiStatus.ACCOUNT_CANCEL_INFO_NOT_EXIST.getReasonPhrase());
}
log.info("检测到注销账号{}昵称{}于{}尝试登录", users.getErbanNo(), userCancelRecord.getNick(), DateTimeUtil.convertDate(userCancelRecord.getUpdateTime()));
CustomOAuth2Exception exception = new CustomOAuth2Exception(CustomOAuth2Exception.ACCOUNT_CANCEL, BusiStatus.ACCOUNT_CANCEL.getReasonPhrase());
exception.addAdditionalInformation("erbanNo", String.valueOf(users.getErbanNo()));
exception.addAdditionalInformation("cancelDate", String.valueOf(userCancelRecord.getUpdateTime().getTime()));
exception.addAdditionalInformation("nick", userCancelRecord.getNick());
exception.addAdditionalInformation("avatar", userCancelRecord.getAvatar());
Integer surviveTime = Integer.valueOf(sysConfService.getDefaultSysConfValueById(Constant.SysConfId.USER_RECOVER_CREDENTIALS_SURVIVE_TIME, String.valueOf(3 * 60)));
//写入凭证标识
jedisService.setex(RedisKey.cancel_user_recover_credentials.getKey(String.valueOf(users.getErbanNo())), surviveTime, String.valueOf(uid));
throw exception;
}
private DayIpMaxRegisterLimitConfig getIpMaxLimitConfig() {
String config = sysConfService.getSysConfValueById(Constant.SysConfId.IP_MAX_REGISTER_LIMIT_CONFIG);
if (!StringUtils.hasText(config)) {
throw new ServiceException(BusiStatus.ALREADY_NOTEXISTS_CONFIG);
}
return gson.fromJson(config, DayIpMaxRegisterLimitConfig.class);
}
private RepeatedDeviceIpRegisterLimitConfig getRepeatedDeviceIpLimitConfig() {
String config = sysConfService.getSysConfValueById(Constant.SysConfId.REPEATED_DEVICE_IP_REGISTER_LIMIT_CONFIG);
if (!StringUtils.hasText(config)) {
throw new ServiceException(BusiStatus.ALREADY_NOTEXISTS_CONFIG);
}
return gson.fromJson(config, RepeatedDeviceIpRegisterLimitConfig.class);
}
}

View File

@@ -1,13 +1,14 @@
package com.accompany.oauth.service;
import com.accompany.oauth.dto.AuthCredentials;
import com.accompany.core.model.Account;
import com.accompany.oauth.constant.GrantTypeEnum;
import com.accompany.oauth.constant.LoginTypeEnum;
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 com.accompany.common.device.DeviceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -22,34 +23,85 @@ public class AuthenticationService {
@Autowired
private UserService userService;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private TokenManager tokenManager;
/**
* 用户认证
*
* @param credentials 认证凭据
* 用户认证 - 简化接口(直接传参)
*
* @param grantType 授权类型/手机号/邮箱
* @param password 密码
* @param code 验证码
* @param deviceInfo 设备信息
* @return 认证结果
* @throws AuthenticationException 认证失败
*/
public AuthResult authenticate(AuthCredentials credentials) {
// 1. 根据认证类型进行用户认证
UserDetails userDetails = authenticateUser(credentials);
public AuthResult authenticate(String grantType,
String username, String password,
String phone, String phoneAreaCode, String email,
String code, DeviceInfo deviceInfo) {
GrantTypeEnum grantTypeEnum = GrantTypeEnum.fromCode(grantType);
// 1. 根据授权类型进行用户认证
LoginTypeEnum loginTypeEnum = null;
Account account = null;
switch (grantTypeEnum) {
case PASSWORD:
loginTypeEnum = LoginTypeEnum.ID;
account = userService.authenticateByPassword(username, password, deviceInfo);
break;
case VERIFY_CODE:
loginTypeEnum = LoginTypeEnum.PHONE;
account = userService.authenticateByVerifyCode(phone, phoneAreaCode, code, deviceInfo);
break;
case EMAIL:
loginTypeEnum = LoginTypeEnum.EMAIL;
account = userService.authenticateByEmail(email, code, deviceInfo);
break;
default:
throw new AuthenticationException("不支持的认证类型: " + grantType);
}
// 2. 检查用户状态
userService.checkUserStatus(userDetails);
// 3. 设置客户端ID和权限范围
userDetails.setClientId(credentials.getClientId());
// 4. 生成Token
TokenPair tokenPair = tokenManager.generateToken(userDetails);
// 5. 构建认证结果
return buildAuthResult(tokenPair, userDetails);
userService.checkUserStatus(account);
myUserDetailsService.login(account, loginTypeEnum, deviceInfo, null);
// 3. 生成Token
TokenPair tokenPair = tokenManager.generateToken(account.getUid());
// 4. 构建认证结果
return buildAuthResult(tokenPair, account);
}
/**
* 第三方登录认证 (兼容OAuth2格式)
*
* @param openId 第三方OpenID
* @param unionId UnionID(可选)
* @param idToken ID Token(可选)
* @param deviceInfo 设备信息
* @return 认证结果
* @throws AuthenticationException 认证失败
*/
public AuthResult authenticateByThirdParty(Byte type, String openId, String unionId, String idToken, DeviceInfo deviceInfo) {
// 1. 第三方认证
Account account = userService.authenticateByOpenId(type, openId, unionId, idToken, deviceInfo);
// 2. 检查用户状态
userService.checkUserStatus(account);
// 4. 生成Token
TokenPair tokenPair = tokenManager.generateToken(account.getUid());
// 5. 构建认证结果
return buildAuthResult(tokenPair, account);
}
/**
* 验证Token
*
@@ -60,32 +112,6 @@ public class AuthenticationService {
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
*
@@ -95,99 +121,13 @@ public class AuthenticationService {
tokenManager.revokeToken(token);
}
/**
* 注销用户所有Token
*
* @param userId 用户ID
*/
public void revokeAllTokens(Long userId) {
tokenManager.revokeTokensByUserId(userId);
}
/**
* 第三方登录认证 (兼容OAuth2格式)
*
* @param openId 第三方OpenID
* @param thirdPartyType 第三方类型
* @param unionId UnionID(可选)
* @param idToken ID Token(可选)
* @param deviceInfo 设备信息
* @param ipAddress IP地址
* @param clientId 客户端标识
* @return 认证结果
* @throws AuthenticationException 认证失败
*/
public AuthResult authenticateByThirdParty(String openId, Integer thirdPartyType,
String unionId, String idToken,
Object deviceInfo, String ipAddress,
String clientId) {
try {
// 1. 第三方认证
UserDetails userDetails = userService.authenticateByOpenId(openId, thirdPartyType.byteValue());
// 2. 检查用户状态
userService.checkUserStatus(userDetails);
// 3. 设置客户端信息
userDetails.setClientId(clientId);
// 4. 生成Token
TokenPair tokenPair = tokenManager.generateToken(userDetails);
// 5. 构建认证结果
return buildAuthResult(tokenPair, userDetails);
} catch (Exception e) {
throw new AuthenticationException("第三方登录失败: " + e.getMessage());
}
}
/**
* 根据认证类型进行用户认证
*
* @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());
private AuthResult buildAuthResult(TokenPair tokenPair, Account account) {
// 构建认证结果
AuthResult authResult = new AuthResult();
authResult.setAccessToken(tokenPair.getAccessToken());
@@ -195,16 +135,10 @@ public class AuthenticationService {
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: 需要根据实际业务填充
authResult.setUserToken(""); // TODO: 需要根据实际业务填充
authResult.setLoginKey(""); // TODO: 需要根据实际业务填充
authResult.setUserToken(""); // TODO: 需要根据实际业务填充
authResult.setLoginKey(""); // TODO: 需要根据实际业务填充
authResult.setUid(account.getUid());
authResult.setNetEaseToken(account.getNeteaseToken());
return authResult;
}
@@ -239,4 +173,5 @@ public class AuthenticationService {
}
return localPart.substring(0, 2) + "***@" + parts[1];
}
}

View File

@@ -0,0 +1,219 @@
package com.accompany.oauth.service;
import com.accompany.common.constant.Constant;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.redis.RedisKey;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.CommonUtil;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.model.Account;
import com.accompany.core.model.AccountLoginRecord;
import com.accompany.core.model.PrettyNumberRecord;
import com.accompany.core.model.Users;
import com.accompany.core.mybatismapper.PrettyNumberRecordMapper;
import com.accompany.core.service.SysConfService;
import com.accompany.core.service.account.AccountBlockCheckService;
import com.accompany.core.service.account.AccountService;
import com.accompany.core.service.account.LoginRecordService;
import com.accompany.core.service.account.UserAppService;
import com.accompany.core.service.common.JedisService;
import com.accompany.core.service.region.RegionNetworkService;
import com.accompany.core.service.user.UsersBaseService;
import com.accompany.email.service.EmailService;
import com.accompany.oauth.constant.LoginTypeEnum;
import com.accompany.sms.service.SmsService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class MyUserDetailsService {
@Autowired
private JedisService jedisService;
@Autowired
private AccountService accountService;
@Autowired
private AccountManageService accountManageService;
@Autowired
private AccountBlockCheckService accountBlockCheckService;
@Autowired
private UserAppService userAppService;
@Autowired
private LoginRecordService loginRecordService;
@Autowired
private UsersBaseService usersBaseService;
@Autowired
private SmsService smsService;
@Autowired
private EmailService emailService;
@Autowired
private SysConfService sysConfService;
@Autowired
private RegionNetworkService regionService;
@Autowired
private PrettyNumberRecordMapper prettyNumberRecordMapper;
/**
* 不允许登录的用户账号类型
*/
private final static List<Byte> NEED_INTERCEPT_USER_TYPE = Arrays.asList(Constant.DefUser.OFFICIAL,
Constant.DefUser.ROBOT, Constant.DefUser.GAME_MANAGE_ROBOT);
public void login(Account account, LoginTypeEnum loginType, DeviceInfo deviceInfo, String openId) {
Long uid = account.getUid();
String deviceId = deviceInfo.getDeviceId();
String ip = deviceInfo.getClientIp();
String app = deviceInfo.getApp();
String appVersion = deviceInfo.getAppVersion();
Date date = new Date();
// 拦截指定账号登录
Users users = usersBaseService.getUsersByUid(account.getUid());
if (users != null && NEED_INTERCEPT_USER_TYPE.contains(users.getDefUser())) {
throw new ServiceException(BusiStatus.ILLEGAL_OPERATE);
}
Long blockEndTime = accountBlockCheckService.checkReturnEndTime(account.getErbanNo(), account.getPhone(), account.getEmail(), deviceId, ip);
//检查账号、设备号、号段是否封禁
if (null != blockEndTime){
//todo
//CustomOAuth2Exception exception = new CustomOAuth2Exception(CustomOAuth2Exception.ACCOUNT_ERROR, "");
//Integer partitionId = null != users? users.getPartitionId(): PartitionEnum.ENGLISH.getId();
//exception.addAdditionalInformation("reason", I18NMessageSourceUtil.getMessage(ACCOUNT_LOGIN_BLOCK_MSG, new Object[]{account.getErbanNo()}, partitionId));
//exception.addAdditionalInformation("date", String.valueOf(blockEndTime));
//throw exception;
}
accountManageService.checkAccountCancel(uid);
//更新account信息
String newToken = accountService.refreshAndGetNetEaseToken(account);
account.setNeteaseToken(newToken);
account.setApp(deviceInfo.getApp());
account.setAppVersion(deviceInfo.getAppVersion());
account.setChannel(deviceInfo.getChannel());
account.setLinkedmeChannel(deviceInfo.getLinkedmeChannel());
account.setDeviceId(deviceInfo.getDeviceId());
account.setImei(deviceInfo.getImei());
account.setIspType(deviceInfo.getIspType());
account.setModel(deviceInfo.getModel());
account.setNetType(deviceInfo.getNetType());
account.setOs(deviceInfo.getOs());
account.setOsversion(deviceInfo.getOsVersion());
account.setUpdateTime(date);
account.setLastLoginTime(date);
account.setLastLoginIp(ip);
accountService.updateById(account);
//更新用户正在使用的app字段
userAppService.updateCurrentApp(uid, app, date, ip, appVersion);
//将用户信息登记
AccountLoginRecord accountLoginRecord = buildAccountLoginRecord(ip, account, loginType.getValue(), deviceInfo, openId);
loginRecordService.addAccountLoginRecordAsync(accountLoginRecord);
}
public AccountLoginRecord buildAccountLoginRecord(String ipAddress, Account account, byte loginType, DeviceInfo deviceInfo, String openId) {
AccountLoginRecord accountLoginRecord = new AccountLoginRecord();
accountLoginRecord.setLoginIp(ipAddress);
accountLoginRecord.setLoginIpRegion(regionService.getRegion(ipAddress));
accountLoginRecord.setUid(account.getUid());
accountLoginRecord.setErbanNo(account.getErbanNo());
accountLoginRecord.setLoginType(loginType);
accountLoginRecord.setDeviceId(deviceInfo.getDeviceId());
accountLoginRecord.setPhone(account.getPhone());
accountLoginRecord.setEmail(account.getEmail());
accountLoginRecord.setApp(account.getApp());
accountLoginRecord.setAppVersion(deviceInfo.getAppVersion());
accountLoginRecord.setIspType(deviceInfo.getIspType());
accountLoginRecord.setModel(deviceInfo.getModel());
accountLoginRecord.setOs(deviceInfo.getOs());
accountLoginRecord.setOsversion(deviceInfo.getOsVersion());
accountLoginRecord.setCreateTime(new Date());
if (loginType == LoginTypeEnum.APPLE.getValue()) {
accountLoginRecord.setAppleUid(openId);
}
return accountLoginRecord;
}
/**
* 普通用户需要用手机验证码登录,官方账号和公会账号不校验验证码
*
* @param account
* @param code
*/
public void checkCodeByUserType(Account account, String code, LoginTypeEnum loginType) {
//是否手机号登录
boolean needVerifyCode = LoginTypeEnum.PHONE.getValue() == loginType.getValue()
|| LoginTypeEnum.EMAIL.getValue() == loginType.getValue();
if (!needVerifyCode) {
return;
}
if (!StringUtils.hasText(code)) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.VERIFY_CODE_ERROR,
// BusiStatus.VERIFY_CODE_ERROR.getReasonPhrase());
}
boolean verifyResult = LoginTypeEnum.PHONE.getValue() == loginType.getValue()?
smsService.verifySmsCode(account.getPhone(), code):
emailService.verifyEmailCode(account.getEmail(), code);
if (!verifyResult) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.VERIFY_CODE_ERROR,
// BusiStatus.VERIFY_CODE_ERROR.getReasonPhrase());
}
}
/**
* 处理密码登录
*
* @param username 用户登录账号
* @param accountPassword
* @return 错误提示
*/
public void validPwd(String username, String password, String accountPassword) {
String value = sysConfService.getDefaultSysConfValueById(Constant.SysConfId.PWD_LOGIN_DAY_WRONG_COUNT, "5");
Long maxCount = Long.valueOf(value);
String cacheKey = RedisKey.user_login_pwd_wrong_day_count.getKey();
Boolean exits = jedisService.exits(cacheKey);
String countValue = jedisService.hget(cacheKey, username);
Long currCount = StringUtils.isEmpty(countValue) ? 0L : Long.parseLong(countValue);
if (currCount >= maxCount) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.PWD_WRONG_OVER_LIMIT, BusiStatus.PWD_WRONG_OVER_LIMIT.getReasonPhrase());
}
if (!password.equals(accountPassword)) {
currCount = jedisService.hincrBy(cacheKey, username, 1L);
if (!exits) {
jedisService.expire(cacheKey, 10 * 60);//10分钟后解锁
}
if (currCount >= maxCount) {
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.PWD_WRONG_OVER_LIMIT, BusiStatus.PWD_WRONG_OVER_LIMIT.getReasonPhrase());
} else {
Long remainCount = maxCount - currCount;
//todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.PASSWORD_ERROR, String.format(BusiStatus.PASSWORD_ERROR_COUNT.getReasonPhrase(), remainCount));
}
}
}
}

View File

@@ -1,8 +1,19 @@
package com.accompany.oauth.service;
import com.accompany.common.constant.Constant;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.CommonUtil;
import com.accompany.common.utils.DESUtils;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.model.Account;
import com.accompany.core.util.KeyStore;
import com.accompany.core.util.MD5;
import com.accompany.oauth.constant.LoginTypeEnum;
import com.accompany.oauth.constant.UserStatus;
import com.accompany.oauth.exception.AuthenticationException;
import com.accompany.oauth.model.UserDetails;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@@ -16,110 +27,80 @@ import java.util.HashSet;
*/
@Service
public class UserService {
@Autowired
private AccountManageService accountManageService;
@Autowired
private MyUserDetailsService myUserDetailsService;
/**
* 通过密码认证用户
*
* @param phone 手机号
* @param password 密码
*
* @param username 手机号
* @param password 密码
* @param deviceInfo
* @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;
@SneakyThrows
public Account authenticateByPassword(String username, String password, DeviceInfo deviceInfo) {
username = DESUtils.DESAndBase64Decrypt(username, KeyStore.DES_ENCRYPT_KEY);
password = DESUtils.DESAndBase64Decrypt(password, KeyStore.DES_ENCRYPT_KEY);
password = MD5.getMD5(password);
Account account = accountManageService.getAccountPyUsername(username, password);
if (account == null) {
// todo
//throw new CustomOAuth2Exception(CustomOAuth2Exception.USER_NOT_EXISTED,
// BusiStatus.USER_NOT_EXISTED.getReasonPhrase());
}
throw AuthenticationException.invalidCredentials();
myUserDetailsService.validPwd(username, password, account.getPassword());
return account;
}
/**
* 通过验证码认证用户
*
* @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;
public Account authenticateByVerifyCode(String phone, String phoneAreaCode, String code, DeviceInfo deviceInfo) {
if (!CommonUtil.checkPhoneFormat(phoneAreaCode, phone)){
throw new ServiceException(BusiStatus.PARAMERROR);
}
throw AuthenticationException.invalidVerifyCode();
Account account = accountManageService.getOrGenAccountByPhone(phone, phoneAreaCode, code, deviceInfo);
//校验验证码
myUserDetailsService.checkCodeByUserType(account, code, LoginTypeEnum.PHONE);
return account;
}
/**
* 通过邮箱认证用户
*
* @param email 邮箱
* @param code 验证码
*
* @param email 邮箱
* @param code 验证码
* @param deviceInfo
* @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();
public Account authenticateByEmail(String email, String code, DeviceInfo deviceInfo) {
Account account = accountManageService.getOrGenAccountByEmail(email, code, deviceInfo, deviceInfo.getClientIp());
//校验验证码
myUserDetailsService.checkCodeByUserType(account, code, LoginTypeEnum.EMAIL);
return account;
}
/**
* 通过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();
public Account authenticateByOpenId(Byte type, String openId, String unionId, String idToken, DeviceInfo deviceInfo) {
return accountManageService.getOrGenAccountByOpenid(type, openId, unionId, idToken, deviceInfo);
}
/**
@@ -147,21 +128,17 @@ public class UserService {
/**
* 检查用户状态是否可用
*
* @param userDetails 用户详情
*
* @throws AuthenticationException 用户不可用
*/
public void checkUserStatus(UserDetails userDetails) {
if (userDetails == null) {
throw AuthenticationException.userNotFound();
}
if (userDetails.getStatus() == UserStatus.FROZEN) {
public void checkUserStatus(Account account) {
if (Constant.AccountState.block.equals(account.getState())) {
throw AuthenticationException.userFrozen();
}
if (userDetails.getStatus() == UserStatus.DELETED) {
if (Constant.AccountState.cancel.equals(account.getState())) {
throw AuthenticationException.userNotFound();
}
}
}

View File

@@ -1,7 +1,5 @@
package com.accompany.oauth.ticket;
import com.accompany.oauth.model.UserDetails;
import com.accompany.oauth.util.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;

View File

@@ -1,63 +0,0 @@
package com.accompany.oauth.ticket;
import com.accompany.oauth.model.UserDetails;
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.concurrent.TimeUnit;
/**
* Redis票据存储实现
* 迁移自OAuth2模块的RedisTicketStore
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Component
public class RedisTicketStore implements TicketStore {
private static final String TICKET_PREFIX = "oauth:ticket:";
private static final String ACCESS_TOKEN_PREFIX = "oauth:uid_access_token:";
@Autowired
private RedissonClient redissonClient;
@Override
public void storeTicket(Ticket ticket, UserDetails userDetails) {
Long uid = userDetails.getUserId();
// 存储用户的ticket
String ticketKey = TICKET_PREFIX + uid;
RBucket<String> ticketBucket = redissonClient.getBucket(ticketKey);
ticketBucket.set(ticket.getValue(), ticket.getExpiresIn(), TimeUnit.SECONDS);
}
@Override
public String readTicket(String key) {
String ticketKey = TICKET_PREFIX + key;
RBucket<String> bucket = redissonClient.getBucket(ticketKey);
return bucket.get();
}
@Override
public String readAccessToken(String key) {
String accessTokenKey = ACCESS_TOKEN_PREFIX + key;
RBucket<String> bucket = redissonClient.getBucket(accessTokenKey);
return bucket.get();
}
/**
* 存储用户的access token
*
* @param uid 用户ID
* @param accessToken 访问令牌
* @param expiresIn 过期时间(秒)
*/
public void storeAccessToken(Long uid, String accessToken, long expiresIn) {
String accessTokenKey = ACCESS_TOKEN_PREFIX + uid;
RBucket<String> bucket = redissonClient.getBucket(accessTokenKey);
bucket.set(accessToken, expiresIn, TimeUnit.SECONDS);
}
}

View File

@@ -1,7 +1,5 @@
package com.accompany.oauth.ticket;
import com.accompany.oauth.model.UserDetails;
/**
* Ticket增强器接口
* 迁移自OAuth2模块的TicketEnhancer

View File

@@ -2,7 +2,6 @@ package com.accompany.oauth.ticket;
import com.accompany.oauth.manager.TokenManager;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.model.UserDetails;
import com.accompany.oauth.service.UserService;
import com.accompany.oauth.exception.TokenException;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -1,7 +1,5 @@
package com.accompany.oauth.ticket;
import com.accompany.oauth.model.UserDetails;
/**
* Ticket存储接口
* 迁移自OAuth2模块的TicketStore

View File

@@ -1,8 +1,11 @@
package com.accompany.oauth.util;
import com.accompany.common.utils.UUIDUtil;
import com.accompany.oauth.config.OAuthConfig;
import com.accompany.oauth.exception.TokenException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -11,6 +14,7 @@ import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
/**
* JWT工具类 - 使用更安全的实现
@@ -20,36 +24,39 @@ import java.util.Set;
*/
@Component
public class JwtUtil {
@Value("${oauth.jwt.secret:accompany-oauth-secret-key-for-jwt-token-generation}")
private String secret;
@Autowired
private OAuthConfig oAuthConfig;
@Value("${oauth.jwt.access-token-expiration:7200}")
private long accessTokenExpiration;
@Value("${oauth.jwt.refresh-token-expiration:2592000}")
private long refreshTokenExpiration;
public long getAccessTokenExpiration() {
return accessTokenExpiration;
}
public long getRefreshTokenExpiration() {
return 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));
this.secretKey = Keys.hmacShaKeyFor(oAuthConfig.getJwtSignKey().getBytes(StandardCharsets.UTF_8));
}
/**
* 生成访问令牌 (兼容OAuth2格式)
*
* @param userId 用户ID
* @param clientId 客户端ID
* @param scopes 权限范围
* @return JWT令牌
*/
public String generateAccessToken(Long userId, String clientId, Set<String> scopes) {
public String generateAccessToken(Long userId) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpiration * 1000);
@@ -57,20 +64,15 @@ public class JwtUtil {
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiration)
.claim("client_id", clientId)
.claim("client_id", oAuthConfig.getClientId())
.claim("token_type", "access_token")
.claim("uid", userId) // 兼容OAuth2
.claim("user_name", userId.toString()) // 兼容OAuth2
.claim("authorities", "oauth2") // 兼容OAuth2
.claim("jti", generateJti()) // 兼容OAuth2
.claim("scope", "read write")
.signWith(secretKey, SignatureAlgorithm.HS256);
if (scopes != null && !scopes.isEmpty()) {
builder.claim("scope", String.join(" ", scopes));
} else {
builder.claim("scope", "read write"); // 默认权限
}
return builder.compact();
}
@@ -78,10 +80,9 @@ public class JwtUtil {
* 生成刷新令牌
*
* @param userId 用户ID
* @param clientId 客户端ID
* @return JWT令牌
*/
public String generateRefreshToken(Long userId, String clientId) {
public String generateRefreshToken(Long userId) {
Date now = new Date();
Date expiration = new Date(now.getTime() + refreshTokenExpiration * 1000);
@@ -89,7 +90,7 @@ public class JwtUtil {
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiration)
.claim("client_id", clientId)
.claim("client_id", oAuthConfig.getClientId())
.claim("token_type", "refresh_token")
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
@@ -116,39 +117,6 @@ public class JwtUtil {
}
}
/**
* 从令牌中获取用户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);
}
/**
* 检查令牌是否过期
*
@@ -175,30 +143,12 @@ public class JwtUtil {
return claims.getExpiration();
}
/**
* 获取访问令牌有效期(秒)
*
* @return 有效期秒数
*/
public long getAccessTokenExpiration() {
return accessTokenExpiration;
}
/**
* 获取刷新令牌有效期(秒)
*
* @return 有效期秒数
*/
public long getRefreshTokenExpiration() {
return refreshTokenExpiration;
}
/**
* 生成JWT Token ID (兼容OAuth2)
*
* @return JTI
*/
private String generateJti() {
return java.util.UUID.randomUUID().toString().replace("-", "");
return UUIDUtil.get();
}
}

View File

@@ -5,6 +5,8 @@ import com.accompany.common.device.DeviceInfo;
import com.accompany.common.result.BusiResult;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.DESUtils;
import com.accompany.common.utils.IPUtils;
import com.accompany.core.base.DeviceInfoContextHolder;
import com.accompany.core.base.UidContextHolder;
import com.accompany.core.util.KeyStore;
import com.accompany.oauth.dto.AuthResult;
@@ -12,6 +14,7 @@ import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.service.AccountManageService;
import com.accompany.oauth.service.AuthenticationService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -26,6 +29,7 @@ import java.util.Map;
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Slf4j
@RestController
@RequestMapping("/acc")
public class AccountController {
@@ -38,46 +42,27 @@ public class AccountController {
@Autowired
private AccountManageService accountManageService;
/**
* 用户注销 (兼容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 openId OpenID
* @param type 第三方类型
* @param unionid UnionID(可选)
* @param deviceInfo 设备信息
* @param app 应用类型
* @param unionId UnionID(可选)
* @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("第三方登录功能暂未实现");
public BusiResult<AuthResult> thirdLogin(
Byte type,
@RequestParam("openid") String openId,
@RequestParam("openid")String unionId,
String idToken) {
log.info("/acc/third/login? app {} , type {}, unionId {}", type, unionId);
DeviceInfo deviceInfo = DeviceInfoContextHolder.get();
AuthResult authResult = authenticationService.authenticateByThirdParty(type, openId, unionId, idToken, deviceInfo);
return BusiResult.success(authResult);
}
/**
@@ -136,7 +121,21 @@ public class AccountController {
throw new UnsupportedOperationException("密码修改功能暂未实现");
}
/**
* 用户注销 (兼容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();
}
/**
* 从请求中提取Token
*

View File

@@ -4,11 +4,8 @@ import com.accompany.common.result.BusiResult;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.IPUtils;
import com.accompany.core.base.DeviceInfoContextHolder;
import com.accompany.oauth.constant.GrantTypeEnum;
import com.accompany.oauth.constant.OAuthConstants;
import com.accompany.oauth.dto.AuthCredentials;
import com.accompany.core.exception.ServiceException;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.dto.TokenRequest;
import com.accompany.common.device.DeviceInfo;
import com.accompany.oauth.service.AuthenticationService;
import com.accompany.oauth.ticket.TicketService;
@@ -36,76 +33,40 @@ public class OAuthController {
private TicketService ticketService;
/**
* 统一认证端点 - 获取Token (兼容OAuth2格式)
* Token获取接口 - 简化实现 (兼容OAuth2格式)
* 支持表单提交和JSON提交两种方式
*
* @param grantType 授权类型
* @param phone 手机号/用户名
* @param email 邮箱
* @param grantType 授权类型 (password, verify_code, email等)
* @param password 密码
* @param code 验证码
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @param phoneAreaCode 电话区号
* @return 直接返回AuthResult结构兼容OAuth2的CustomOAuth2AccessToken
*/
@PostMapping("/token")
public AuthResult token(@RequestParam(value = "grant_type", required = false) String grantType,
@RequestParam(value = "client_id", required = false) String clientId,
@RequestParam(value = "client_secret", required = false) String clientSecret,
@RequestParam(value = "phone", required = false) String phone,
@RequestParam(value = "phoneAreaCode", required = false) String phoneAreaCode,
@RequestParam(value = "password", required = false) String password,
@RequestParam(value = "email", required = false) String email,
@RequestParam(value = "code", required = false) String code) {
DeviceInfo deviceInfo = DeviceInfoContextHolder.get();
// 支持两种格式表单提交和JSON提交
TokenRequest request = new TokenRequest();
request.setGrantType(grantType);
request.setUsername(StringUtils.hasText(phone) ? phone : email);
request.setPassword(password);
request.setCode(code);
request.setClientId(clientId);
request.setClientSecret(clientSecret);
request.setDeviceId(deviceInfo.getDeviceId());
// 1. 验证请求参数
validateTokenRequest(request);
public AuthResult token(@RequestParam(value = "grant_type") String grantType,
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestParam(value = "phone") String phone,
@RequestParam(value = "phoneAreaCode") String phoneAreaCode,
@RequestParam(value = "email") String email,
@RequestParam(value = "code") String code) {
// 2. 构建认证凭据
AuthCredentials credentials = buildAuthCredentials(request, phoneAreaCode, deviceInfo);
// 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("缺少刷新令牌");
// 1. 简单验证
if (!StringUtils.hasText(grantType)) {
throw new ServiceException(BusiStatus.PARAMERROR);
}
// 执行Token刷新
AuthResult authResult = authenticationService.refreshToken(request.getRefreshToken());
// 2. 获取设备信息
DeviceInfo deviceInfo = DeviceInfoContextHolder.get();
return authResult;
// 3. 直接调用服务,不做中间转换
return authenticationService.authenticate(grantType,
username, password, phone, phoneAreaCode, email, code, deviceInfo);
}
/**
* Ticket签发端点 (兼容OAuth2格式)
* Ticket签发接口 - 简化实现 (兼容OAuth2格式)
*
* @param issueType 签发类型
* @param issueType 签发类型 (once/multi)
* @param accessToken 访问令牌
* @param httpRequest HTTP请求
* @return BusiResult包装的Ticket响应
@@ -115,16 +76,17 @@ public class OAuthController {
@RequestParam("access_token") String accessToken,
HttpServletRequest httpRequest) {
try {
// 验证签发类型
if (!"once".equals(issueType) && !"multi".equals(issueType)) {
throw new IllegalArgumentException("unsupported ticket issue type");
throw new IllegalArgumentException("不支持的票据签发类型");
}
// 直接传递accessToken给TicketService
Map<String, Object> result = ticketService.issueTicket(accessToken);
// 获取IP地址并保存登录记录
// 获取IP地址并异步记录用户登录信息
String ipAddress = IPUtils.getRealIpAddress(httpRequest);
Long uid = (Long) result.get("uid");
ticketService.saveLoginRecord(uid, ipAddress, null);
return BusiResult.success(result);
@@ -133,62 +95,6 @@ public class OAuthController {
}
}
/**
* 验证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()) &&
!OAuthConstants.GrantType.EMAIL.equals(request.getGrantType())) {
throw new IllegalArgumentException("缺少username参数");
}
if (!StringUtils.hasText(request.getPassword()) &&
!OAuthConstants.GrantType.REFRESH_TOKEN.equals(request.getGrantType()) &&
!OAuthConstants.GrantType.EMAIL.equals(request.getGrantType())) {
throw new IllegalArgumentException("缺少password参数");
}
if (!StringUtils.hasText(request.getCode()) &&
OAuthConstants.GrantType.EMAIL.equals(request.getGrantType())) {
throw new IllegalArgumentException("缺少验证码参数");
}
}
/**
* 构建认证凭据
*
* @param request Token请求
* @param phoneAreaCode 电话区号
* @return 认证凭据
*/
private AuthCredentials buildAuthCredentials(TokenRequest request,
String phoneAreaCode, DeviceInfo deviceInfo) {
AuthCredentials credentials = new AuthCredentials();
// 设置认证类型
GrantTypeEnum grantTypeEnum = GrantTypeEnum.fromCode(request.getGrantType());
credentials.setType(grantTypeEnum);
// 设置主体和凭据
credentials.setPrincipal(request.getUsername());
credentials.setCredentials(request.getPassword());
// 设置客户端信息
credentials.setClientId(request.getClientId());
// 设置权限范围
credentials.setScope(OAuthConstants.Scope.ALL);
credentials.setDeviceInfo(deviceInfo);
return credentials;
}
}

View File

@@ -36,47 +36,6 @@ public class OAuth2CompatibilityIntegrationTest {
@Autowired
private ObjectMapper objectMapper;
/**
* 测试OAuth2 Token端点兼容性 - 表单格式
*/
@Test
public void testOAuth2TokenEndpoint_FormFormat() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("127.0.0.1");
try {
AuthResult result = oauthController.token(
"password", // grant_type
"testuser", // phone
null, // email
"password123", // password
null, // code
"test-client", // client_id
"read write", // scope
"device-001"
);
// 验证OAuth2兼容的响应结构
assertNotNull(result);
assertNotNull(result.getAccessToken());
assertNotNull(result.getRefreshToken());
assertNotNull(result.getTokenType());
assertEquals("bearer", result.getTokenType().toLowerCase());
assertTrue(result.getExpiresIn() > 0);
// 验证OAuth2特有字段
assertNotNull(result.getUserToken());
assertNotNull(result.getLoginKey());
System.out.println("OAuth2 Token Response: " + objectMapper.writeValueAsString(result));
} catch (Exception e) {
// 在测试环境中,由于没有真实的用户服务实现,预期会抛出异常
assertTrue(e.getMessage().contains("用户") || e.getMessage().contains("User") ||
e.getMessage().contains("认证") || e.getMessage().contains("authentication"));
}
}
/**
* 测试账户管理端点兼容性
*/
@@ -153,30 +112,6 @@ public class OAuth2CompatibilityIntegrationTest {
assertEquals(authResult.getLoginKey(), deserializedResult.getLoginKey());
}
/**
* 测试Token请求参数验证
*/
@Test
public void testTokenRequestValidation() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("127.0.0.1");
// 测试缺少grant_type
assertThrows(IllegalArgumentException.class, () -> {
oauthController.token(null, "user", null, "pass", null, null, null, null);
});
// 测试缺少username
assertThrows(IllegalArgumentException.class, () -> {
oauthController.token("password", null, null, null, null, null, null, null);
});
// 测试缺少password
assertThrows(IllegalArgumentException.class, () -> {
oauthController.token("password", "user", null, null, null, null, null, null);
});
}
/**
* 验证支持的grant_type类型
*/