fix(sso): inline FQN→import + harden auto-create orphan rollback

- Replace inline fully-qualified names with top-of-file imports across
  SsoService / SsoStateService / FeishuSsoProvider (ObjectMapper, Autowired,
  Map.of, Date, DuplicateKeyException, Mac, URLEncoder) per code style.
- createSsoUser: roll back the freshly inserted user on any non-duplicate
  identity-insert failure, preventing passwordless orphan accounts. The two
  inserts share no transaction — the method is self-invoked and the enclosing
  callback performs a network call, so a method-level @Transactional would not
  apply; an explicit rollback in the catch is the correct guard here.
This commit is contained in:
matevip 2026-06-26 10:07:46 +08:00
parent 03a6d61131
commit b048718298
3 changed files with 24 additions and 8 deletions

View File

@ -2,8 +2,10 @@ package vip.mate.auth.sso;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@ -46,9 +48,9 @@ public class SsoService {
private final AuthService authService;
private final SsoProperties ssoProperties;
private final BCryptPasswordEncoder passwordEncoder;
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
private final ObjectMapper objectMapper;
/** Optional — audit may be null in narrow test contexts. */
@org.springframework.beans.factory.annotation.Autowired(required = false)
@Autowired(required = false)
private AuditEventService auditService;
// ==================== authorize ====================
@ -228,6 +230,16 @@ public class SsoService {
503, "SSO 登录遇到并发冲突, 请重试");
}
return createSsoUser(providerId, info, true);
} catch (RuntimeException e) {
// Identity insert failed for a non-duplicate reason (e.g. transient DB error).
// The two inserts are not in a shared transaction this method is self-invoked
// and the enclosing callback performs a network call, so a method-level
// @Transactional would not apply. Roll back the freshly inserted user here so we
// never leave a passwordless orphan account behind.
if (newUser.getId() != null) {
userMapper.deleteById(newUser.getId());
}
throw e;
}
}
@ -240,7 +252,7 @@ public class SsoService {
private void audit(String action, String provider, String externalId, Long userId) {
if (auditService != null) {
try {
String detail = objectMapper.writeValueAsString(java.util.Map.of(
String detail = objectMapper.writeValueAsString(Map.of(
"provider", provider != null ? provider : "",
"userId", userId != null ? userId : "null"));
auditService.record(action, "sso",

View File

@ -8,6 +8,7 @@ import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import vip.mate.auth.sso.model.SsoStateEntity;
@ -15,10 +16,12 @@ import vip.mate.auth.sso.provider.SsoUserInfo;
import vip.mate.auth.sso.repository.SsoStateMapper;
import vip.mate.exception.MateClawException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@ -126,8 +129,8 @@ public class SsoStateService {
.claim("externalId", info.externalId())
.claim("unionId", info.unionId())
.claim("externalName", info.displayName())
.issuedAt(new java.util.Date(now))
.expiration(new java.util.Date(now + BIND_TOKEN_TTL_SECONDS * 1000L))
.issuedAt(new Date(now))
.expiration(new Date(now + BIND_TOKEN_TTL_SECONDS * 1000L))
.signWith(getSignKey())
.compact();
}
@ -166,7 +169,7 @@ public class SsoStateService {
consumed.setCreatedAt(LocalDateTime.now());
try {
stateMapper.insert(consumed);
} catch (org.springframework.dao.DuplicateKeyException e) {
} catch (DuplicateKeyException e) {
throw new MateClawException("err.sso.bind_token_used",
400, "bind_token 已被使用, 请重新登录");
}
@ -213,7 +216,7 @@ public class SsoStateService {
private String hmacSha256Hex(String input) {
try {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(getSignKey());
byte[] hash = mac.doFinal(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();

View File

@ -7,6 +7,7 @@ import vip.mate.auth.sso.SsoProperties;
import vip.mate.exception.MateClawException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@ -221,7 +222,7 @@ public class FeishuSsoProvider implements SsoProvider {
private static String encode(String s) {
try {
return java.net.URLEncoder.encode(s, "UTF-8");
return URLEncoder.encode(s, "UTF-8");
} catch (Exception e) {
return s;
}