oauth-重写-ticket和密码重置

This commit is contained in:
2025-09-21 14:05:53 +08:00
parent 600c439a43
commit 5ad43fd617
21 changed files with 446 additions and 756 deletions

View File

@@ -5,7 +5,6 @@ import com.accompany.common.constant.Constant;
import com.accompany.common.constant.SmsConstant;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.redis.RedisKey;
import com.accompany.common.result.BusiResult;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.RandomUtil;
import com.accompany.common.utils.StringUtils;

View File

@@ -0,0 +1,46 @@
package com.accompany.oauth.dto;
import com.accompany.oauth.ticket.Ticket;
import lombok.Data;
import java.util.List;
/**
* 票据签发响应VO
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Data
public class TicketResponseVO {
/**
* 票据列表
*/
private List<TicketVO> tickets;
/**
* 用户ID
*/
private Long uid;
/**
* 签发类型
*/
private String issue_type = Ticket.MULTI_TYPE;
/**
* 票据信息VO
*/
@Data
public static class TicketVO {
/**
* 票据值
*/
private String ticket;
/**
* 过期时间(秒)
*/
private Integer expiresIn;
}
}

View File

@@ -48,10 +48,9 @@ public class TokenValidation {
this.valid = valid;
}
public static TokenValidation valid(Long userId, Set<String> scopes, Date expirationTime, String clientId) {
public static TokenValidation valid(Long userId, Date expirationTime, String clientId) {
TokenValidation validation = new TokenValidation(true);
validation.setUserId(userId);
validation.setScopes(scopes);
validation.setExpirationTime(expirationTime);
validation.setClientId(clientId);
return validation;

View File

@@ -1,22 +1,21 @@
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.core.util.StringUtils;
import com.accompany.oauth.exception.TokenException;
import com.accompany.oauth.model.TokenPair;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.util.JwtUtil;
import io.jsonwebtoken.Claims;
import org.redisson.api.RBucket;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.beans.factory.InitializingBean;
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存储和管理
@@ -25,40 +24,34 @@ import java.util.Set;
* @since 1.0.0
*/
@Component
public class TokenManager {
public class TokenManager implements InitializingBean {
@Autowired
public JwtUtil jwtUtil;
@Autowired
private RedissonClient redissonClient;
@Autowired
private JedisService jedisService;
private RMapCache<Long, String> tokenCache;
private final String defaultScope = "read write";
public TokenPair generateToken(Long uid) {
try {
// 生成JWT token
String accessToken = jwtUtil.generateAccessToken(uid);
String refreshToken = jwtUtil.generateRefreshToken(uid);
// 生成JWT token
Date now = new Date();
String accessToken = jwtUtil.generateAccessToken(uid, now);
String refreshToken = jwtUtil.generateRefreshToken(uid, now);
// 存储access token
jedisService.hwrite(RedisKey.uid_access_token.getKey(), uid.toString(), accessToken);
// 存储access token
tokenCache.fastPut(uid, accessToken, jwtUtil.getAccessTokenExpiration(), TimeUnit.SECONDS);
// 存储用户的access token到ticket store
jedisService.hwrite(RedisKey.uid_ticket.getKey(), uid.toString(), accessToken);
TokenPair tokenPair = new TokenPair();
tokenPair.setAccessToken(accessToken);
tokenPair.setRefreshToken(refreshToken);
tokenPair.setExpiresIn(jwtUtil.getAccessTokenExpiration());
tokenPair.setTokenType("Bearer");
tokenPair.setScope(defaultScope);
TokenPair tokenPair = new TokenPair();
tokenPair.setAccessToken(accessToken);
tokenPair.setRefreshToken(refreshToken);
//todo
tokenPair.setExpiresIn(jwtUtil.getAccessTokenExpiration());
tokenPair.setTokenType("Bearer");
tokenPair.setScope(String.join(" ", "read", "write"));
return tokenPair;
} catch (Exception e) {
throw TokenException.tokenGenerationFailed();
}
return tokenPair;
}
/**
@@ -71,24 +64,19 @@ public class TokenManager {
try {
// 首先验证JWT格式和签名
Claims claims = jwtUtil.validateAndParseToken(token);
// 提取token信息
Long uid = Long.valueOf(claims.getSubject());
// 检查Redis中是否存在该token
String accessTokenKey = OAuthConstants.Token.ACCESS_TOKEN_PREFIX + token;
RBucket<String> bucket = redissonClient.getBucket(accessTokenKey);
if (!bucket.isExists()) {
String cacheToken = tokenCache.get(uid);
if (StringUtils.isBlank(cacheToken) || !cacheToken.equals(token)) {
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);
return TokenValidation.valid(uid, expirationTime, clientId);
} catch (TokenException e) {
return TokenValidation.invalid(e.getErrorDescription());
} catch (Exception e) {
@@ -102,18 +90,15 @@ public class TokenManager {
* @param token 访问令牌
*/
public void revokeToken(String token) {
try {
Claims claims = jwtUtil.validateAndParseToken(token);
Long uid = Long.valueOf(claims.getSubject());
// 删除access token
jedisService.hdel(RedisKey.uid_access_token.getKey(), uid.toString());
// ticket
jedisService.hdel(RedisKey.uid_ticket.getKey(), uid.toString());
Claims claims = jwtUtil.validateAndParseToken(token);
Long uid = Long.valueOf(claims.getSubject());
} catch (Exception e) {
// 忽略撤销失败的情况
}
// 删除access token
tokenCache.fastRemove(uid);
}
@Override
public void afterPropertiesSet() {
tokenCache = redissonClient.getMapCache(RedisKey.uid_access_token.getKey(), StringCodec.INSTANCE);
}
}

View File

@@ -71,7 +71,9 @@ public class AccountManageService {
@Autowired
private GoogleOpenidRefService googleOpenidRefService;
protected Gson gson = new Gson();
public Account getAccountPyUid(Long uid) {
return accountService.getAccountByUid(uid);
}
public Account getAccountPyUsername(String username, String password) {
log.info("getAccountByUserName username:{} password:{}", username, password);
@@ -478,21 +480,23 @@ public class AccountManageService {
if (ObjectUtil.isNull(userCancelRecord)) {
//获取不到注销账号信息
log.info("获取不到用户{}注销信息", uid);
throw new CustomOAuth2Exception(CustomOAuth2Exception.ACCOUNT_CANCEL_INFO_NOT_EXIST, BusiStatus.ACCOUNT_CANCEL_INFO_NOT_EXIST.getReasonPhrase());
//todo
//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());
//todo
//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;
//throw exception;
}
private DayIpMaxRegisterLimitConfig getIpMaxLimitConfig() {
@@ -500,7 +504,7 @@ public class AccountManageService {
if (!StringUtils.hasText(config)) {
throw new ServiceException(BusiStatus.ALREADY_NOTEXISTS_CONFIG);
}
return gson.fromJson(config, DayIpMaxRegisterLimitConfig.class);
return JSON.parseObject(config, DayIpMaxRegisterLimitConfig.class);
}
private RepeatedDeviceIpRegisterLimitConfig getRepeatedDeviceIpLimitConfig() {
@@ -508,7 +512,7 @@ public class AccountManageService {
if (!StringUtils.hasText(config)) {
throw new ServiceException(BusiStatus.ALREADY_NOTEXISTS_CONFIG);
}
return gson.fromJson(config, RepeatedDeviceIpRegisterLimitConfig.class);
return JSON.parseObject(config, RepeatedDeviceIpRegisterLimitConfig.class);
}
}

View File

@@ -7,7 +7,6 @@ import com.accompany.oauth.dto.AuthResult;
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.common.device.DeviceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -51,7 +50,7 @@ public class AuthenticationService {
switch (grantTypeEnum) {
case PASSWORD:
loginTypeEnum = LoginTypeEnum.ID;
account = userService.authenticateByPassword(username, password, deviceInfo);
account = userService.authenticateByPassword(username, password);
break;
case VERIFY_CODE:
loginTypeEnum = LoginTypeEnum.PHONE;
@@ -101,16 +100,6 @@ public class AuthenticationService {
// 5. 构建认证结果
return buildAuthResult(tokenPair, account);
}
/**
* 验证Token
*
* @param token 访问令牌
* @return Token验证结果
*/
public TokenValidation validateToken(String token) {
return tokenManager.validateToken(token);
}
/**
* 注销Token
@@ -142,36 +131,5 @@ public class AuthenticationService {
return authResult;
}
/**
* 手机号脱敏
*
* @param phone 原始手机号
* @return 脱敏后的手机号
*/
private String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
/**
* 邮箱脱敏
*
* @param email 原始邮箱
* @return 脱敏后的邮箱
*/
private String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String localPart = parts[0];
if (localPart.length() <= 2) {
return email;
}
return localPart.substring(0, 2) + "***@" + parts[1];
}
}

View File

@@ -38,12 +38,11 @@ public class UserService {
*
* @param username 手机号
* @param password 密码
* @param deviceInfo
* @return 用户详情
* @throws AuthenticationException 认证失败
*/
@SneakyThrows
public Account authenticateByPassword(String username, String password, DeviceInfo deviceInfo) {
public Account authenticateByPassword(String username, String password) {
username = DESUtils.DESAndBase64Decrypt(username, KeyStore.DES_ENCRYPT_KEY);
password = DESUtils.DESAndBase64Decrypt(password, KeyStore.DES_ENCRYPT_KEY);
password = MD5.getMD5(password);
@@ -106,24 +105,12 @@ public class UserService {
/**
* 根据用户ID获取用户详情
*
* @param userId 用户ID
* @param uid 用户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();
public Account getUserByUid(Long uid) {
return accountManageService.getAccountPyUid(uid);
}
/**

View File

@@ -78,10 +78,6 @@ public class DefaultTicket implements Ticket, Serializable {
return expiration;
}
public void setExpiresIn(int delta) {
setExpiration(new Date(System.currentTimeMillis() + delta * 1000L));
}
@Override
public int getExpiresIn() {
return expiration != null ? Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)

View File

@@ -1,92 +0,0 @@
package com.accompany.oauth.ticket;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT票据增强器
* 迁移自OAuth2模块的JwtTicketConverter
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@Component
public class JwtTicketEnhancer implements TicketEnhancer {
@Value("${oauth.jwt.secret:accompany-oauth-secret-key-for-jwt-token-generation}")
private String secret;
@Autowired
private ObjectMapper objectMapper;
@Override
public Ticket enhance(Ticket ticket, UserDetails userDetails) {
DefaultTicket result = new DefaultTicket(ticket);
result.setValue(encode(ticket, userDetails));
return result;
}
/**
* 编码票据为JWT
*
* @param ticket 票据
* @param userDetails 用户详情
* @return JWT字符串
*/
protected String encode(Ticket ticket, UserDetails userDetails) {
try {
Map<String, Object> claims = convertTicket(ticket, userDetails);
// 确保密钥长度足够
String key = secret;
if (key.getBytes(StandardCharsets.UTF_8).length < 32) {
key = key + "0123456789abcdef0123456789abcdef";
}
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
Date now = new Date();
Date expiration = ticket.getExpiration();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(secretKey, SignatureAlgorithm.HS256);
return builder.compact();
} catch (Exception e) {
throw new IllegalStateException("Cannot convert ticket to JWT", e);
}
}
/**
* 转换票据为Claims
*
* @param ticket 票据
* @param userDetails 用户详情
* @return Claims Map
*/
protected Map<String, Object> convertTicket(Ticket ticket, UserDetails userDetails) {
Map<String, Object> response = new HashMap<>();
response.put("ticket_id", ticket.getValue());
response.put("client_id", userDetails.getClientId() != null ? userDetails.getClientId() : "default");
response.put("exp", ticket.getExpiresIn());
response.put("uid", userDetails.getUserId());
response.put("ticket_type", ticket.getTicketType());
if (ticket.getScope() != null) {
response.put("scope", String.join(" ", ticket.getScope()));
}
return response;
}
}

View File

@@ -1,20 +1,83 @@
package com.accompany.oauth.ticket;
import com.accompany.core.model.Account;
import com.accompany.oauth.config.OAuthConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Ticket增强器接口
* 迁移自OAuth2模块的TicketEnhancer
* JWT票据增强器
* 迁移自OAuth2模块的JwtTicketConverter
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public interface TicketEnhancer {
@Component
public class TicketEnhancer {
@Autowired
private OAuthConfig oAuthConfig;
public Ticket enhance(Ticket ticket, Account account) {
DefaultTicket result = new DefaultTicket(ticket);
result.setValue(encode(ticket, account));
return result;
}
/**
* 增强票据(如JWT签名)
* 编码票据为JWT
*
* @param ticket 原始票据
* @param userDetails 用户详情
* @return 增强后的票据
* @param ticket 票据
* @param account
* @return JWT字符串
*/
Ticket enhance(Ticket ticket, UserDetails userDetails);
protected String encode(Ticket ticket, Account account) {
try {
Map<String, Object> claims = convertTicket(ticket, account);
SecretKeySpec secretKey = new SecretKeySpec(oAuthConfig.getJwtSignKey().getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
Date now = new Date();
Date expiration = ticket.getExpiration();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(secretKey, SignatureAlgorithm.HS256);
return builder.compact();
} catch (Exception e) {
throw new IllegalStateException("Cannot convert ticket to JWT", e);
}
}
/**
* 转换票据为Claims
*
* @param ticket 票据
* @param account
* @return Claims Map
*/
protected Map<String, Object> convertTicket(Ticket ticket, Account account) {
Map<String, Object> response = new HashMap<>();
response.put("ticket_id", ticket.getValue());
response.put("client_id", oAuthConfig.getClientId());
response.put("exp", ticket.getExpiresIn());
response.put("uid", account.getUid());
response.put("ticket_type", ticket.getTicketType());
response.put("scope", "read write");
return response;
}
}

View File

@@ -1,13 +1,28 @@
package com.accompany.oauth.ticket;
import com.accompany.common.device.DeviceInfo;
import com.accompany.common.redis.RedisKey;
import com.accompany.core.model.Account;
import com.accompany.core.model.AccountLoginRecord;
import com.accompany.core.service.account.AccountService;
import com.accompany.core.service.account.LoginRecordService;
import com.accompany.core.service.account.UserAppService;
import com.accompany.oauth.constant.LoginTypeEnum;
import com.accompany.oauth.dto.TicketResponseVO;
import com.accompany.oauth.exception.TokenException;
import com.accompany.oauth.manager.TokenManager;
import com.accompany.oauth.model.TokenValidation;
import com.accompany.oauth.service.MyUserDetailsService;
import com.accompany.oauth.service.UserService;
import com.accompany.oauth.exception.TokenException;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Ticket服务
@@ -17,9 +32,7 @@ import java.util.*;
* @since 1.0.0
*/
@Service
public class TicketService {
private final int ticketValiditySeconds = 60 * 60; // ticket过期时间1小时
public class TicketService implements InitializingBean {
@Autowired
private TokenManager tokenManager;
@@ -27,19 +40,29 @@ public class TicketService {
@Autowired
private UserService userService;
@Autowired
private TicketStore ticketStore;
@Autowired
private TicketEnhancer ticketEnhancer;
@Autowired
private UserAppService userAppService;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private LoginRecordService loginRecordService;
@Autowired
private AccountService accountService;
@Autowired
private RedissonClient redissonClient;
private RMapCache<Long, String> ticketCache;
/**
* 签发票据
*
* @param accessToken 访问令牌
* @return 票据响应
*/
public Map<String, Object> issueTicket(String accessToken) {
public TicketResponseVO issueTicket(String accessToken) {
// 1. 验证访问令牌
TokenValidation validation = tokenManager.validateToken(accessToken);
if (!validation.isValid()) {
@@ -47,34 +70,31 @@ public class TicketService {
}
// 2. 获取用户信息
UserDetails userDetails = userService.getUserById(validation.getUserId());
userService.checkUserStatus(userDetails);
// 3. 检查token缓存
String uidStr = userDetails.getUserId().toString();
String realAccessToken = ticketStore.readAccessToken(uidStr);
if (realAccessToken == null || !realAccessToken.equals(accessToken)) {
throw TokenException.invalidToken();
}
Account account = userService.getUserByUid(validation.getUserId());
userService.checkUserStatus(account);
Date expiration = validation.getExpirationTime();
// 4. 创建票据
DefaultTicket defaultTicket = new DefaultTicket(UUID.randomUUID().toString());
defaultTicket.setAccessToken(accessToken);
defaultTicket.setExpiresIn(ticketValiditySeconds);
defaultTicket.setTicketType(Ticket.ONCE_TYPE);
defaultTicket.setExpiration(expiration);
defaultTicket.setTicketType(Ticket.MULTI_TYPE);
defaultTicket.setScope(validation.getScopes());
// 5. 增强票据(JWT签名)
Ticket enhancedTicket = ticketEnhancer.enhance(defaultTicket, userDetails);
Ticket enhancedTicket = ticketEnhancer.enhance(defaultTicket, account);
// 6. 存储票据
ticketStore.storeTicket(enhancedTicket, userDetails);
ticketCache.fastPut(account.getUid(), enhancedTicket.getValue(), defaultTicket.getExpiresIn(), TimeUnit.SECONDS);
// 7. 构建响应
Map<String, Object> response = new HashMap<>();
response.put("tickets", Arrays.asList(createTicketVo(enhancedTicket)));
response.put("uid", userDetails.getUserId());
response.put("issue_type", Ticket.ONCE_TYPE);
TicketResponseVO response = new TicketResponseVO();
response.setUid(account.getUid());
List<TicketResponseVO.TicketVO> tickets = new ArrayList<>();
tickets.add(createTicketVo(enhancedTicket));
response.setTickets(tickets);
return response;
}
@@ -85,12 +105,10 @@ public class TicketService {
* @param ticket 票据
* @return 票据VO
*/
private Map<String, Object> createTicketVo(Ticket ticket) {
Map<String, Object> ticketVo = new HashMap<>();
ticketVo.put("ticket", ticket.getValue());
ticketVo.put("expires_in", ticket.getExpiresIn());
ticketVo.put("ticket_type", ticket.getTicketType());
ticketVo.put("scope", ticket.getScope() != null ? String.join(" ", ticket.getScope()) : "");
private TicketResponseVO.TicketVO createTicketVo(Ticket ticket) {
TicketResponseVO.TicketVO ticketVo = new TicketResponseVO.TicketVO();
ticketVo.setTicket(ticket.getValue());
ticketVo.setExpiresIn(ticket.getExpiresIn());
return ticketVo;
}
@@ -101,8 +119,23 @@ public class TicketService {
* @param ipAddress IP地址
* @param deviceInfo 设备信息
*/
public void saveLoginRecord(Long uid, String ipAddress, Object deviceInfo) {
// TODO: 实现登录记录保存逻辑
// 这里需要根据具体的业务需求来实现
public void saveLoginRecord(Long uid, String ipAddress, DeviceInfo deviceInfo) {
Optional.ofNullable(uid).ifPresent(id -> {
userAppService.updateCurrentApp(uid, deviceInfo.getApp(), new Date(), ipAddress, deviceInfo.getAppVersion());
long count = loginRecordService.countLoginRecordToday(id);
if (count <= 0L) {
Account account = accountService.getAccountByUid(id);
Optional.ofNullable(account).ifPresent(acc -> {
AccountLoginRecord record = myUserDetailsService.buildAccountLoginRecord(ipAddress, acc, LoginTypeEnum.TICKET.getValue(), deviceInfo, null);
loginRecordService.addAccountLoginRecord(record);
});
}
});
}
@Override
public void afterPropertiesSet() {
ticketCache = redissonClient.getMapCache(RedisKey.uid_ticket.getKey(), StringCodec.INSTANCE);
}
}

View File

@@ -1,35 +0,0 @@
package com.accompany.oauth.ticket;
/**
* Ticket存储接口
* 迁移自OAuth2模块的TicketStore
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
public interface TicketStore {
/**
* 存储票据
*
* @param ticket 票据
* @param userDetails 用户详情
*/
void storeTicket(Ticket ticket, UserDetails userDetails);
/**
* 读取票据
*
* @param key 票据键
* @return 票据值
*/
String readTicket(String key);
/**
* 读取访问令牌
*
* @param key 用户键
* @return 访问令牌值
*/
String readAccessToken(String key);
}

View File

@@ -5,16 +5,14 @@ import com.accompany.oauth.config.OAuthConfig;
import com.accompany.oauth.exception.TokenException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
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.Set;
import java.util.UUID;
/**
* JWT工具类 - 使用更安全的实现
@@ -23,42 +21,27 @@ import java.util.UUID;
* @since 1.0.0
*/
@Component
public class JwtUtil {
public class JwtUtil implements InitializingBean {
@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;
}
@Getter
private long accessTokenExpiration = 2592000;
@Getter
private long refreshTokenExpiration = 3196800;
private SecretKey secretKey;
@PostConstruct
public void init() {
// 确保密钥长度足够
this.secretKey = Keys.hmacShaKeyFor(oAuthConfig.getJwtSignKey().getBytes(StandardCharsets.UTF_8));
}
/**
* 生成访问令牌 (兼容OAuth2格式)
*
*
* @param userId 用户ID
* @param now
* @return JWT令牌
*/
public String generateAccessToken(Long userId) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpiration * 1000);
public String generateAccessToken(Long userId, Date now) {
Date expiration = getExpiration(now);
JwtBuilder builder = Jwts.builder()
.setSubject(userId.toString())
@@ -75,15 +58,19 @@ public class JwtUtil {
return builder.compact();
}
public Date getExpiration(Date now){
return new Date(now.getTime() + accessTokenExpiration * 1000);
}
/**
* 生成刷新令牌
*
*
* @param userId 用户ID
* @param now
* @return JWT令牌
*/
public String generateRefreshToken(Long userId) {
Date now = new Date();
public String generateRefreshToken(Long userId, Date now) {
Date expiration = new Date(now.getTime() + refreshTokenExpiration * 1000);
return Jwts.builder()
@@ -117,32 +104,6 @@ public class JwtUtil {
}
}
/**
* 检查令牌是否过期
*
* @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();
}
/**
* 生成JWT Token ID (兼容OAuth2)
*
@@ -151,4 +112,10 @@ public class JwtUtil {
private String generateJti() {
return UUIDUtil.get();
}
@Override
public void afterPropertiesSet() {
// 确保密钥长度足够
this.secretKey = Keys.hmacShaKeyFor(oAuthConfig.getJwtSignKey().getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -52,18 +52,8 @@ public class AuthResultJsonSerializer extends JsonSerializer<AuthResult> {
}
// accid (兼容oauth2)
if (authResult.getAccid() != null && !authResult.getAccid().isEmpty()) {
gen.writeStringField("accid", authResult.getAccid());
}
// userToken (兼容oauth2)
if (authResult.getUserToken() != null && !authResult.getUserToken().isEmpty()) {
gen.writeStringField("userToken", authResult.getUserToken());
}
// loginKey (兼容oauth2)
if (authResult.getLoginKey() != null && !authResult.getLoginKey().isEmpty()) {
gen.writeStringField("loginKey", authResult.getLoginKey());
if (authResult.getUid() != null) {
gen.writeStringField("accid", authResult.getUid().toString());
}
gen.writeEndObject();

View File

@@ -1,10 +1,7 @@
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;
/**
@@ -16,26 +13,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@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" // 图标
);
}
/**
* 配置跨域
*/

View File

@@ -1,27 +1,17 @@
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.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;
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.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户账户控制器
@@ -34,14 +24,8 @@ import java.util.Map;
@RequestMapping("/acc")
public class AccountController {
/** 密码强度检查正则必须包括大小写字母和数字长度为6到16 */
private static final String PASSWORD_REGIX_V2 = "^(?=.*\\d)(?=.*[a-zA-Z]).{6,16}$";
@Autowired
private AuthenticationService authenticationService;
@Autowired
private AccountManageService accountManageService;
/**
* 第三方登录 (兼容OAuth2格式)
@@ -58,69 +42,11 @@ public class AccountController {
@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);
}
/**
* 重置密码 (兼容OAuth2格式)
*
* @return BusiResult响应结果
*/
@SneakyThrows
@PostMapping("/pwd/reset")
public BusiResult<Void> resetPassword(HttpServletRequest request,
String phone, String newPwd, String smsCode) {
// TODO: 实现密码重置逻辑
// 1. 验证用户身份(手机号+验证码或邮箱+验证码)
// 2. 重置密码
// 3. 发送通知
Long uid = UidContextHolder.get();
phone = DESUtils.DESAndBase64Decrypt(phone, KeyStore.DES_ENCRYPT_KEY);
newPwd = DESUtils.DESAndBase64Decrypt(newPwd, KeyStore.DES_ENCRYPT_KEY);
// 密码长度检查
if(!newPwd.matches(PASSWORD_REGIX_V2)){
return new BusiResult<>(BusiStatus.WEAK_PASSWORD);
}
accountManageService.resetPasswordByResetCode(uid, phone, newPwd, smsCode);
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("密码修改功能暂未实现");
}
/**
* 用户注销 (兼容OAuth2格式)
@@ -136,17 +62,4 @@ public class AccountController {
return BusiResult.success();
}
/**
* 从请求中提取Token
*
* @param request HTTP请求
* @return Token字符串
*/
private String extractTokenFromRequest(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
return null;
}
}

View File

@@ -6,15 +6,16 @@ import com.accompany.common.utils.IPUtils;
import com.accompany.core.base.DeviceInfoContextHolder;
import com.accompany.core.exception.ServiceException;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.dto.TicketResponseVO;
import com.accompany.common.device.DeviceInfo;
import com.accompany.oauth.service.AuthenticationService;
import com.accompany.oauth.ticket.Ticket;
import com.accompany.oauth.ticket.TicketService;
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.Map;
/**
* OAuth认证控制器
@@ -72,27 +73,25 @@ public class OAuthController {
* @return BusiResult包装的Ticket响应
*/
@GetMapping("/ticket")
public BusiResult<Map<String, Object>> issueTicket(@RequestParam("issue_type") String issueType,
@RequestParam("access_token") String accessToken,
HttpServletRequest httpRequest) {
try {
// 验证签发类型
if (!"once".equals(issueType) && !"multi".equals(issueType)) {
throw new IllegalArgumentException("不支持的票据签发类型");
}
// 直接传递accessToken给TicketService
Map<String, Object> result = ticketService.issueTicket(accessToken);
// 获取IP地址并异步记录用户登录信息
String ipAddress = IPUtils.getRealIpAddress(httpRequest);
Long uid = (Long) result.get("uid");
ticketService.saveLoginRecord(uid, ipAddress, null);
return BusiResult.success(result);
} catch (Exception e) {
return BusiResult.fail(BusiStatus.IP_REGION_HAD_LIMIT, e.getMessage());
public BusiResult<TicketResponseVO> issueTicket(@RequestParam("issue_type") String issueType,
@RequestParam("access_token") String accessToken,
HttpServletRequest httpRequest) {
// 验证签发类型
if (!Ticket.ONCE_TYPE.equals(issueType) && !Ticket.MULTI_TYPE.equals(issueType)) {
throw new IllegalArgumentException("不支持的票据签发类型");
}
DeviceInfo deviceInfo = DeviceInfoContextHolder.get();
// 直接传递accessToken给TicketService
TicketResponseVO result = ticketService.issueTicket(accessToken);
// 获取IP地址并异步记录用户登录信息
String ipAddress = IPUtils.getRealIpAddress(httpRequest);
Long uid = result.getUid();
ticketService.saveLoginRecord(uid, ipAddress, deviceInfo);
return BusiResult.success(result);
}

View File

@@ -0,0 +1,149 @@
package com.accompany.oauth.controller;
import com.accompany.common.annotation.Authorization;
import com.accompany.common.result.BusiResult;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.DESUtils;
import com.accompany.core.base.UidContextHolder;
import com.accompany.core.exception.ServiceException;
import com.accompany.core.model.Account;
import com.accompany.core.service.account.AccountService;
import com.accompany.core.util.KeyStore;
import com.accompany.oauth.service.AccountManageService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("/acc/pwd")
public class PwdController {
/** 密码强度检查正则必须包括大小写字母和数字长度为6到16 */
private static final String PASSWORD_REGIX_V2 = "^(?=.*\\d)(?=.*[a-zA-Z]).{6,16}$";
@Autowired
private AccountService accountService;
@Autowired
private AccountManageService accountManageService;
/**
* 重置密码接口,用于用户忘记密码,找回密码服务
*
* @param newPwd
* 新密码
* @param smsCode
* 重置码
* @return 1:成功 2重置码无效 3不存在该用户 4其它错误
*/
@PostMapping("/reset")
@SneakyThrows
public BusiResult<Void> resetPassword(String phone, String newPwd, String smsCode) {
if (StringUtils.isBlank(phone) || StringUtils.isBlank(newPwd) || StringUtils.isBlank(smsCode)){
throw new ServiceException(BusiStatus.PARAMERROR);
}
newPwd = DESUtils.DESAndBase64Decrypt(newPwd, KeyStore.DES_ENCRYPT_KEY);
// 密码长度检查
if(!newPwd.matches(PASSWORD_REGIX_V2)){
return new BusiResult<>(BusiStatus.WEAK_PASSWORD);
}
Long uid = UidContextHolder.get();
phone = DESUtils.DESAndBase64Decrypt(phone, KeyStore.DES_ENCRYPT_KEY);
accountManageService.resetPasswordByResetCode(uid, phone, newPwd, smsCode);
return new BusiResult<>(BusiStatus.SUCCESS);
}
/**
* 重置密码接口,用于用户忘记密码,找回密码服务
*
* @param newPwd
* 新密码
* @param email
* 邮箱
* @return 1:成功 2重置码无效 3不存在该用户 4其它错误
*/
@PostMapping("/resetByEmail")
@SneakyThrows
public BusiResult<Void> resetPasswordByEmail(String email, String newPwd, String code) {
if (StringUtils.isBlank(email) || StringUtils.isBlank(newPwd) || StringUtils.isBlank(code)){
throw new ServiceException(BusiStatus.PARAMERROR);
}
Long uid = UidContextHolder.get();
email = DESUtils.DESAndBase64Decrypt(email, KeyStore.DES_ENCRYPT_KEY);
newPwd = DESUtils.DESAndBase64Decrypt(newPwd, KeyStore.DES_ENCRYPT_KEY);
// 密码长度检查
if(!newPwd.matches(PASSWORD_REGIX_V2)){
return new BusiResult<>(BusiStatus.WEAK_PASSWORD);
}
accountManageService.resetPasswordByEmailCode(uid, email, newPwd, code);
return new BusiResult<>(BusiStatus.SUCCESS);
}
/**
* 设置新密码
* @param newPwd
* @return
*/
@Authorization
@PostMapping("/set")
@SneakyThrows
public BusiResult<Void> setupPassword(String newPwd) {
//加入密码DES解密
newPwd = DESUtils.DESAndBase64Decrypt(newPwd, KeyStore.DES_ENCRYPT_KEY);
// 密码长度检查
if(!newPwd.matches(PASSWORD_REGIX_V2)){
return new BusiResult<>(BusiStatus.WEAK_PASSWORD);
}
Long uid = UidContextHolder.get();
accountManageService.setupInitialPassword(uid, newPwd);
return new BusiResult<>(BusiStatus.SUCCESS);
}
@Authorization
@PostMapping("/modify")
@SneakyThrows
public BusiResult<Void> modifyPassword(HttpServletRequest request,
String pwd, String newPwd) {
newPwd = DESUtils.DESAndBase64Decrypt(newPwd, KeyStore.DES_ENCRYPT_KEY);
// 密码长度检查
if(!newPwd.matches(PASSWORD_REGIX_V2)){
return new BusiResult<>(BusiStatus.WEAK_PASSWORD);
}
Long uid = UidContextHolder.get();
// 加入密码DES解密
pwd = DESUtils.DESAndBase64Decrypt(pwd, KeyStore.DES_ENCRYPT_KEY);
Account account = this.accountService.getById(uid);
if (account == null) {
return new BusiResult<>(BusiStatus.INVALID_USER);
}
accountManageService.resetPasswordByOldPassword(account.getPhone(), pwd, newPwd);
return new BusiResult<>(BusiStatus.SUCCESS);
}
}

View File

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

View File

@@ -1,130 +0,0 @@
package com.accompany.oauth;
import com.accompany.oauth.dto.AuthResult;
import com.accompany.oauth.dto.TokenRequest;
import com.accompany.oauth.controller.OAuthController;
import com.accompany.oauth.controller.AccountController;
import com.accompany.common.result.BusiResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ActiveProfiles;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* OAuth2兼容性集成测试
* 验证OAuth模块与OAuth2模块的API兼容性
*
* @author Accompany OAuth Team
* @since 1.0.0
*/
@SpringBootTest
@ActiveProfiles("test")
public class OAuth2CompatibilityIntegrationTest {
@Autowired
private OAuthController oauthController;
@Autowired
private AccountController accountController;
@Autowired
private ObjectMapper objectMapper;
/**
* 测试账户管理端点兼容性
*/
@Test
public void testAccountEndpoints() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("127.0.0.1");
try {
// 测试登出端点
BusiResult<Void> logoutResult = accountController.logout("mock-token");
} catch (Exception e) {
assertTrue(e.getMessage().contains("token") || e.getMessage().contains("Token") ||
e.getMessage().contains("无效") || e.getMessage().contains("invalid"));
}
}
/**
* 测试Ticket端点兼容性
*/
@Test
public void testTicketEndpoint() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("127.0.0.1");
try {
BusiResult<Map<String, Object>> ticketResult = oauthController.issueTicket(
"once", "mock-access-token", request
);
} catch (Exception e) {
// 预期异常因为token无效
assertTrue(e.getMessage().contains("token") || e.getMessage().contains("Token") ||
e.getMessage().contains("无效") || e.getMessage().contains("invalid"));
}
}
/**
* 测试AuthResult的JSON序列化兼容性
*/
@Test
public void testAuthResultSerialization() throws Exception {
AuthResult authResult = new AuthResult();
authResult.setAccessToken("test-access-token");
authResult.setRefreshToken("test-refresh-token");
authResult.setTokenType("bearer");
authResult.setExpiresIn(3600L);
authResult.setScope("read write");
authResult.setUserToken("user-token-123");
authResult.setLoginKey("login-key-456");
String json = objectMapper.writeValueAsString(authResult);
System.out.println("AuthResult JSON: " + json);
// 验证JSON包含OAuth2标准字段
assertTrue(json.contains("access_token"));
assertTrue(json.contains("refresh_token"));
assertTrue(json.contains("token_type"));
assertTrue(json.contains("expires_in"));
assertTrue(json.contains("scope"));
// 验证兼容字段
assertTrue(json.contains("user_token"));
assertTrue(json.contains("login_key"));
// 验证反序列化
AuthResult deserializedResult = objectMapper.readValue(json, AuthResult.class);
assertEquals(authResult.getAccessToken(), deserializedResult.getAccessToken());
assertEquals(authResult.getRefreshToken(), deserializedResult.getRefreshToken());
assertEquals(authResult.getTokenType(), deserializedResult.getTokenType());
assertEquals(authResult.getExpiresIn(), deserializedResult.getExpiresIn());
assertEquals(authResult.getUserToken(), deserializedResult.getUserToken());
assertEquals(authResult.getLoginKey(), deserializedResult.getLoginKey());
}
/**
* 验证支持的grant_type类型
*/
@Test
public void testSupportedGrantTypes() {
String[] supportedTypes = {"password", "sms_code", "email_code", "third_party", "refresh_token"};
for (String grantType : supportedTypes) {
TokenRequest request = new TokenRequest();
request.setGrantType(grantType);
assertNotNull(request.getGrantType());
assertEquals(grantType, request.getGrantType());
}
}
}

View File

@@ -62,13 +62,12 @@ public class JwtTicketConverter implements TicketEnhancer,TicketCoverter {
protected String encode(Ticket ticket, OAuth2Authentication authentication, AccountDetails userDetails) {
String content;
try {
content = objectMapper.writeValueAsString(convertTicket(ticket, authentication,userDetails));
content = objectMapper.writeValueAsString(convertTicket(ticket, authentication, userDetails));
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
return JwtHelper.encode(content, signer).getEncoded();
}
}