package com.talentcard.server.business.auth;
|
|
import java.nio.charset.StandardCharsets;
|
import java.security.MessageDigest;
|
import java.security.SecureRandom;
|
import java.time.LocalDateTime;
|
import java.util.Base64;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@Service
|
public class TalentAuthService {
|
|
private static final SecureRandom RANDOM = new SecureRandom();
|
private static final int ACCESS_TOKEN_HOURS = 2;
|
private static final int REFRESH_TOKEN_DAYS = 30;
|
|
private final JdbcTemplate jdbc;
|
private final WeChatMiniProgramClient weChat;
|
|
public TalentAuthService(JdbcTemplate jdbc, WeChatMiniProgramClient weChat) {
|
this.jdbc = jdbc;
|
this.weChat = weChat;
|
}
|
|
@Transactional
|
public Map<String, Object> login(TalentLoginRequest request) {
|
String openId = weChat.exchangeOpenId(request.wechatCode());
|
String mobile = weChat.exchangePhoneNumber(request.phoneCode());
|
return establishSession(openId, mobile);
|
}
|
|
@Transactional
|
public Map<String, Object> createDevelopmentSession(long talentId) {
|
List<Map<String, Object>> talents = rows("""
|
select id, openid
|
from talent_user
|
where id = ? and deleted = 0 and status = 1
|
limit 1 for update
|
""", talentId);
|
if (talents.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "开发测试人才账号不存在或不可用");
|
Object openId = talents.get(0).get("openid");
|
if (openId == null || String.valueOf(openId).isBlank()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "开发测试人才账号未配置微信标识");
|
}
|
return issueSession(talentId);
|
}
|
|
@Transactional
|
public Map<String, Object> refresh(TalentRefreshRequest request) {
|
List<Map<String, Object>> sessions = rows("""
|
select s.id, s.talent_id
|
from talent_session s
|
join talent_user t on t.id = s.talent_id and t.deleted = 0 and t.status = 1
|
where s.refresh_token_hash = ? and s.revoked_at is null and s.refresh_expires_at > now()
|
limit 1 for update
|
""", sha256(request.refreshToken().trim()));
|
if (sessions.isEmpty()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录");
|
Map<String, Object> session = sessions.get(0);
|
String accessToken = token();
|
String refreshToken = token();
|
LocalDateTime accessExpiresAt = LocalDateTime.now().plusHours(ACCESS_TOKEN_HOURS);
|
LocalDateTime refreshExpiresAt = LocalDateTime.now().plusDays(REFRESH_TOKEN_DAYS);
|
jdbc.update("""
|
update talent_session
|
set access_token_hash = ?, refresh_token_hash = ?, access_expires_at = ?, refresh_expires_at = ?, last_used_at = now()
|
where id = ?
|
""", sha256(accessToken), sha256(refreshToken), accessExpiresAt, refreshExpiresAt, session.get("id"));
|
return sessionResult(number(session.get("talentId")), accessToken, refreshToken, accessExpiresAt);
|
}
|
|
@Transactional
|
public Map<String, Object> logout(String authorization) {
|
String token = bearerToken(authorization);
|
if (!token.isBlank()) {
|
jdbc.update("update talent_session set revoked_at = coalesce(revoked_at, now()) where access_token_hash = ?", sha256(token));
|
}
|
return Map.of("loggedOut", true);
|
}
|
|
@Transactional(readOnly = true)
|
public TalentSessionPrincipal authenticate(String authorization) {
|
String token = bearerToken(authorization);
|
if (token.isBlank()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "请先登录");
|
List<Map<String, Object>> sessions = rows("""
|
select s.talent_id
|
from talent_session s
|
join talent_user t on t.id = s.talent_id and t.deleted = 0 and t.status = 1
|
where s.access_token_hash = ? and s.revoked_at is null and s.access_expires_at > now()
|
limit 1
|
""", sha256(token));
|
if (sessions.isEmpty()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录");
|
return new TalentSessionPrincipal(number(sessions.get(0).get("talentId")));
|
}
|
|
private Map<String, Object> establishSession(String openId, String mobile) {
|
List<Map<String, Object>> talents = rows("""
|
select id, openid
|
from talent_user
|
where mobile = ? and deleted = 0 and status = 1
|
limit 1 for update
|
""", mobile);
|
if (talents.isEmpty()) return issueSession(registerVisitor(openId, mobile));
|
Map<String, Object> talent = talents.get(0);
|
long talentId = number(talent.get("id"));
|
List<Map<String, Object>> binding = rows("""
|
select t.id,
|
exists(select 1 from talent_card c where c.talent_id = t.id and c.deleted = 0 and c.card_type = 'PERSONAL') has_talent_card
|
from talent_user t
|
where t.openid = ? and t.deleted = 0
|
limit 1 for update
|
""", openId);
|
if (!binding.isEmpty() && number(binding.get(0).get("id")) != talentId) {
|
if (number(binding.get(0).get("hasTalentCard")) != 0) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前微信已绑定其他人才账号,请联系管理员");
|
}
|
jdbc.update("update talent_user set openid = null where id = ?", number(binding.get(0).get("id")));
|
}
|
jdbc.update("update talent_user set openid = ? where id = ?", openId, talentId);
|
return issueSession(talentId);
|
}
|
|
private long registerVisitor(String openId, String mobile) {
|
List<Map<String, Object>> bindings = rows("""
|
select id
|
from talent_user
|
where openid = ? and deleted = 0 and status = 1
|
limit 1 for update
|
""", openId);
|
if (!bindings.isEmpty()) {
|
long talentId = number(bindings.get(0).get("id"));
|
jdbc.update("update talent_user set mobile = ? where id = ?", mobile, talentId);
|
return talentId;
|
}
|
jdbc.update("""
|
insert into talent_user(openid, name, mobile, talent_level, source_area_type, status)
|
values (?, '普通游客', ?, '普通游客', 'LOCAL', 1)
|
""", openId, mobile);
|
Long talentId = jdbc.queryForObject("select last_insert_id()", Long.class);
|
if (talentId == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "普通游客注册失败");
|
return talentId;
|
}
|
|
private Map<String, Object> issueSession(long talentId) {
|
jdbc.update("update talent_session set revoked_at = now() where talent_id = ? and revoked_at is null", talentId);
|
|
String accessToken = token();
|
String refreshToken = token();
|
LocalDateTime accessExpiresAt = LocalDateTime.now().plusHours(ACCESS_TOKEN_HOURS);
|
LocalDateTime refreshExpiresAt = LocalDateTime.now().plusDays(REFRESH_TOKEN_DAYS);
|
jdbc.update("""
|
insert into talent_session(talent_id, access_token_hash, refresh_token_hash, access_expires_at, refresh_expires_at, last_used_at)
|
values (?, ?, ?, ?, ?, now())
|
""", talentId, sha256(accessToken), sha256(refreshToken), accessExpiresAt, refreshExpiresAt);
|
return sessionResult(talentId, accessToken, refreshToken, accessExpiresAt);
|
}
|
|
private Map<String, Object> sessionResult(long talentId, String accessToken, String refreshToken, LocalDateTime accessExpiresAt) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("talentId", talentId);
|
result.put("accessToken", accessToken);
|
result.put("refreshToken", refreshToken);
|
result.put("expireAt", accessExpiresAt.atZone(java.time.ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli());
|
return result;
|
}
|
|
private String token() {
|
byte[] bytes = new byte[32];
|
RANDOM.nextBytes(bytes);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
}
|
|
private String bearerToken(String authorization) {
|
return authorization == null ? "" : authorization.replaceFirst("(?i)^Bearer\\s+", "").trim();
|
}
|
|
private String sha256(String value) {
|
try {
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
|
StringBuilder result = new StringBuilder();
|
for (byte item : digest) result.append(String.format("%02x", item));
|
return result.toString();
|
} catch (Exception exception) {
|
throw new IllegalStateException("Unable to process session token", exception);
|
}
|
}
|
|
private List<Map<String, Object>> rows(String sql, Object... args) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, args));
|
}
|
|
private long number(Object value) {
|
if (value instanceof Number number) return number.longValue();
|
try { return Long.parseLong(String.valueOf(value)); } catch (Exception exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "人才会话数据异常"); }
|
}
|
}
|