oauth-重写-ticket和密码重置
This commit is contained in:
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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];
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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" // 图标
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置跨域
|
||||
*/
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user