|
|
@@ -0,0 +1,95 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.etotem.cfc.entity.SsoTicket;
|
|
|
+import com.etotem.cfc.mapper.SsoTicketMapper;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.security.SecureRandom;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 跨小程序单点登录一次性票据服务(cfc ↔ train 双向免登)。
|
|
|
+ * cfc 与 train 共用本库(zxyj),ticket 由签发方写入,目标方原子占用(used 0→1)。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class SsoService {
|
|
|
+
|
|
|
+ /** 系统标识:cfc */
|
|
|
+ public static final String SYS_CFC = "cfc";
|
|
|
+ /** 系统标识:train */
|
|
|
+ public static final String SYS_TRAIN = "train";
|
|
|
+
|
|
|
+ private static final SecureRandom RANDOM = new SecureRandom();
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private SsoTicketMapper ssoTicketMapper;
|
|
|
+
|
|
|
+ /** 票据有效期(毫秒),默认 5 分钟 */
|
|
|
+ @Value("${sso.ticket-ttl-ms:300000}")
|
|
|
+ private long ticketTtlMs;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 签发一次性票据。
|
|
|
+ *
|
|
|
+ * @param sourceSystem 签发方系统标识(cfc / train)
|
|
|
+ * @param targetSystem 目标系统标识(cfc / train)
|
|
|
+ * @param sourceUserId 签发方用户ID(cfc=users.id / train=train_user.id)
|
|
|
+ * @param cfcUserId cfc 统一会员ID(users.id),跨系统换票依据
|
|
|
+ * @return 票据字符串(64 位十六进制)
|
|
|
+ */
|
|
|
+ public String issueTicket(String sourceSystem, String targetSystem, Long sourceUserId, Long cfcUserId) {
|
|
|
+ SsoTicket t = new SsoTicket();
|
|
|
+ t.setTicket(generateTicket());
|
|
|
+ t.setSourceSystem(sourceSystem);
|
|
|
+ t.setTargetSystem(targetSystem);
|
|
|
+ t.setSourceUserId(sourceUserId);
|
|
|
+ t.setCfcUserId(cfcUserId);
|
|
|
+ t.setUsed(0);
|
|
|
+ t.setExpireAt(new Date(System.currentTimeMillis() + ticketTtlMs));
|
|
|
+ t.setCreatedAt(new Date());
|
|
|
+ ssoTicketMapper.insert(t);
|
|
|
+ return t.getTicket();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 原子消费票据:仅当「票据未使用 + 未过期 + 目标系统匹配」时置 used=1。
|
|
|
+ * 并发/重放场景下若多请求同时命中,只有 update 影响行数=1 的那一个成功。
|
|
|
+ *
|
|
|
+ * @return 成功返回 cfc_user_id;无效/已用/过期/目标不符返回 null
|
|
|
+ */
|
|
|
+ public Long consumeTicket(String ticket, String targetSystem) {
|
|
|
+ if (ticket == null || ticket.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ int updated = ssoTicketMapper.update(null, new LambdaUpdateWrapper<SsoTicket>()
|
|
|
+ .eq(SsoTicket::getTicket, ticket)
|
|
|
+ .eq(SsoTicket::getTargetSystem, targetSystem)
|
|
|
+ .eq(SsoTicket::getUsed, 0)
|
|
|
+ .gt(SsoTicket::getExpireAt, new Date())
|
|
|
+ .set(SsoTicket::getUsed, 1));
|
|
|
+ if (updated != 1) {
|
|
|
+ log.warn("SSO 票据消费失败(无效/已用/过期/目标不符): target={}", targetSystem);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ SsoTicket t = ssoTicketMapper.selectOne(new LambdaQueryWrapper<SsoTicket>()
|
|
|
+ .eq(SsoTicket::getTicket, ticket));
|
|
|
+ return t != null ? t.getCfcUserId() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateTicket() {
|
|
|
+ byte[] bytes = new byte[32];
|
|
|
+ RANDOM.nextBytes(bytes);
|
|
|
+ StringBuilder sb = new StringBuilder(64);
|
|
|
+ for (byte b : bytes) {
|
|
|
+ sb.append(Character.forDigit((b >> 4) & 0xF, 16));
|
|
|
+ sb.append(Character.forDigit(b & 0xF, 16));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+}
|