2026-04-25 16:41:10 +08:00
|
|
|
package com.xuqm.demo.service;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
import com.xuqm.common.exception.BusinessException;
|
2026-04-27 23:41:58 +08:00
|
|
|
import com.xuqm.common.security.AppRequestSignatureUtil;
|
2026-04-25 16:41:10 +08:00
|
|
|
import com.xuqm.common.security.JwtUtil;
|
|
|
|
|
import com.xuqm.demo.entity.DemoUserEntity;
|
|
|
|
|
import com.xuqm.demo.repository.DemoUserRepository;
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
2026-04-27 23:41:58 +08:00
|
|
|
import org.springframework.http.HttpEntity;
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
2026-04-25 16:41:10 +08:00
|
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
import org.springframework.web.client.RestClientException;
|
|
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
|
|
|
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
public class DemoAuthService {
|
|
|
|
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(DemoAuthService.class);
|
|
|
|
|
|
|
|
|
|
private final DemoUserRepository userRepository;
|
|
|
|
|
private final JwtUtil jwtUtil;
|
|
|
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
|
private final RestTemplate restTemplate;
|
2026-04-27 23:41:58 +08:00
|
|
|
private final DemoAppSecretClient appSecretClient;
|
2026-04-25 16:41:10 +08:00
|
|
|
|
|
|
|
|
@Value("${demo.im-service-url:http://xuqm-im-service:8082}")
|
|
|
|
|
private String imServiceUrl;
|
|
|
|
|
|
|
|
|
|
public DemoAuthService(DemoUserRepository userRepository,
|
|
|
|
|
JwtUtil jwtUtil,
|
|
|
|
|
PasswordEncoder passwordEncoder,
|
2026-04-27 23:41:58 +08:00
|
|
|
RestTemplate restTemplate,
|
|
|
|
|
DemoAppSecretClient appSecretClient) {
|
2026-04-25 16:41:10 +08:00
|
|
|
this.userRepository = userRepository;
|
|
|
|
|
this.jwtUtil = jwtUtil;
|
|
|
|
|
this.passwordEncoder = passwordEncoder;
|
|
|
|
|
this.restTemplate = restTemplate;
|
2026-04-27 23:41:58 +08:00
|
|
|
this.appSecretClient = appSecretClient;
|
2026-04-25 16:41:10 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 09:45:20 +08:00
|
|
|
public record AuthResult(String demoToken, long demoTokenExpiresAt, String imToken, long imTokenExpiresAt, UserProfile profile) {}
|
|
|
|
|
public record ImCredential(String token, long expiresAt) {}
|
2026-04-25 16:41:10 +08:00
|
|
|
|
|
|
|
|
public record UserProfile(String appId, String userId, String nickname, String avatar, String gender) {}
|
|
|
|
|
|
|
|
|
|
@Transactional
|
|
|
|
|
public AuthResult register(String appId, String userId, String password, String nickname) {
|
|
|
|
|
if (userRepository.existsByAppIdAndUserId(appId, userId)) {
|
|
|
|
|
throw new BusinessException(409, "User already exists: " + userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DemoUserEntity user = new DemoUserEntity();
|
|
|
|
|
user.setId(UUID.randomUUID().toString());
|
|
|
|
|
user.setAppId(appId);
|
|
|
|
|
user.setUserId(userId);
|
|
|
|
|
user.setPasswordHash(passwordEncoder.encode(password));
|
|
|
|
|
user.setNickname(nickname != null ? nickname : userId);
|
|
|
|
|
user.setGender(DemoUserEntity.Gender.UNKNOWN);
|
|
|
|
|
user.setCreatedAt(Instant.now());
|
|
|
|
|
userRepository.save(user);
|
|
|
|
|
|
|
|
|
|
String demoToken = generateDemoToken(appId, userId);
|
2026-04-28 09:45:20 +08:00
|
|
|
ImCredential imCredential = callImServiceLogin(appId, userId, user.getNickname());
|
|
|
|
|
|
|
|
|
|
return new AuthResult(
|
|
|
|
|
demoToken,
|
|
|
|
|
tokenExpiresAt(),
|
|
|
|
|
imCredential.token(),
|
|
|
|
|
imCredential.expiresAt(),
|
|
|
|
|
toProfile(user)
|
|
|
|
|
);
|
2026-04-25 16:41:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Transactional(readOnly = true)
|
|
|
|
|
public AuthResult login(String appId, String userId, String password) {
|
|
|
|
|
DemoUserEntity user = userRepository.findByAppIdAndUserId(appId, userId)
|
|
|
|
|
.orElseThrow(() -> new BusinessException(401, "Invalid credentials"));
|
|
|
|
|
|
|
|
|
|
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
|
|
|
|
throw new BusinessException(401, "Invalid credentials");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String demoToken = generateDemoToken(appId, userId);
|
2026-04-28 09:45:20 +08:00
|
|
|
ImCredential imCredential = callImServiceLogin(appId, userId, user.getNickname());
|
|
|
|
|
|
|
|
|
|
return new AuthResult(
|
|
|
|
|
demoToken,
|
|
|
|
|
tokenExpiresAt(),
|
|
|
|
|
imCredential.token(),
|
|
|
|
|
imCredential.expiresAt(),
|
|
|
|
|
toProfile(user)
|
|
|
|
|
);
|
2026-04-25 16:41:10 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 11:57:46 +08:00
|
|
|
@Transactional
|
|
|
|
|
public void resetPassword(String appId, String userId, String newPassword) {
|
|
|
|
|
DemoUserEntity user = userRepository.findByAppIdAndUserId(appId, userId)
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "User not found: " + userId));
|
|
|
|
|
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
|
|
|
|
userRepository.save(user);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 16:41:10 +08:00
|
|
|
private String generateDemoToken(String appId, String userId) {
|
|
|
|
|
return jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 09:45:20 +08:00
|
|
|
private long tokenExpiresAt() {
|
|
|
|
|
return Instant.now().toEpochMilli() + jwtUtil.getExpirationMillis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ImCredential refreshImToken(String appId, String userId) {
|
|
|
|
|
DemoUserEntity user = userRepository.findByAppIdAndUserId(appId, userId)
|
|
|
|
|
.orElseThrow(() -> new BusinessException(404, "User not found: " + userId));
|
|
|
|
|
return callImServiceLogin(appId, userId, user.getNickname());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 16:41:10 +08:00
|
|
|
/**
|
|
|
|
|
* Calls im-service to ensure the IM account exists and obtain an IM token.
|
2026-04-27 23:41:58 +08:00
|
|
|
* POST {imServiceUrl}/api/im/auth/login?appId={appId}&userId={userId}&nickname={nickname}
|
2026-04-25 16:41:10 +08:00
|
|
|
* Response: {"code":200,"data":{"token":"..."}}
|
|
|
|
|
*/
|
2026-04-28 09:45:20 +08:00
|
|
|
private ImCredential callImServiceLogin(String appId, String userId, String nickname) {
|
2026-04-27 23:41:58 +08:00
|
|
|
long timestamp = System.currentTimeMillis();
|
|
|
|
|
String nonce = UUID.randomUUID().toString();
|
|
|
|
|
String effectiveNickname = nickname != null ? nickname : userId;
|
|
|
|
|
String appSecret = appSecretClient.getAppSecret(appId);
|
|
|
|
|
String payload = AppRequestSignatureUtil.payload(appId, userId, effectiveNickname, null, timestamp, nonce);
|
|
|
|
|
String signature = AppRequestSignatureUtil.sign(appSecret, payload);
|
|
|
|
|
|
2026-04-25 16:41:10 +08:00
|
|
|
String url = UriComponentsBuilder.fromHttpUrl(imServiceUrl)
|
|
|
|
|
.path("/api/im/auth/login")
|
|
|
|
|
.queryParam("appId", appId)
|
|
|
|
|
.queryParam("userId", userId)
|
2026-04-27 23:41:58 +08:00
|
|
|
.queryParam("nickname", effectiveNickname)
|
2026-04-25 16:41:10 +08:00
|
|
|
.toUriString();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-27 23:41:58 +08:00
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.set("X-App-Timestamp", String.valueOf(timestamp));
|
|
|
|
|
headers.set("X-App-Nonce", nonce);
|
|
|
|
|
headers.set("X-App-Signature", signature);
|
|
|
|
|
ResponseEntity<JsonNode> response = restTemplate.exchange(
|
|
|
|
|
url,
|
|
|
|
|
HttpMethod.POST,
|
|
|
|
|
new HttpEntity<>(headers),
|
|
|
|
|
JsonNode.class
|
|
|
|
|
);
|
|
|
|
|
JsonNode body = response.getBody();
|
|
|
|
|
if (body != null && body.path("code").asInt() == 200) {
|
2026-04-28 09:45:20 +08:00
|
|
|
JsonNode data = body.path("data");
|
|
|
|
|
String token = data.path("token").asText(null);
|
|
|
|
|
if (token == null || token.isBlank()) {
|
|
|
|
|
throw new BusinessException(502, "Failed to refresh IM token");
|
|
|
|
|
}
|
|
|
|
|
return new ImCredential(
|
|
|
|
|
token,
|
|
|
|
|
data.path("expiresAt").asLong(tokenExpiresAt())
|
|
|
|
|
);
|
2026-04-25 16:41:10 +08:00
|
|
|
}
|
2026-04-27 23:41:58 +08:00
|
|
|
log.warn("im-service login returned unexpected response for appId={} userId={}: {}", appId, userId, body);
|
2026-04-28 09:45:20 +08:00
|
|
|
throw new BusinessException(502, "Failed to refresh IM token");
|
2026-04-25 16:41:10 +08:00
|
|
|
} catch (RestClientException e) {
|
|
|
|
|
log.error("Failed to call im-service login for appId={} userId={}: {}", appId, userId, e.getMessage());
|
2026-04-28 09:45:20 +08:00
|
|
|
throw new BusinessException(502, "Failed to refresh IM token");
|
2026-04-25 16:41:10 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private UserProfile toProfile(DemoUserEntity user) {
|
|
|
|
|
return new UserProfile(
|
|
|
|
|
user.getAppId(),
|
|
|
|
|
user.getUserId(),
|
|
|
|
|
user.getNickname(),
|
|
|
|
|
user.getAvatar(),
|
|
|
|
|
user.getGender() != null ? user.getGender().name() : DemoUserEntity.Gender.UNKNOWN.name()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|