|
|
|
|
@@ -3,64 +3,423 @@ package com.k12study.auth.service;
|
|
|
|
|
import com.k12study.api.auth.dto.CurrentUserResponse;
|
|
|
|
|
import com.k12study.api.auth.dto.LoginRequest;
|
|
|
|
|
import com.k12study.api.auth.dto.TokenResponse;
|
|
|
|
|
import com.k12study.common.security.config.AuthProperties;
|
|
|
|
|
import com.k12study.common.security.context.RequestUserContextHolder;
|
|
|
|
|
import com.k12study.common.security.jwt.JwtTokenProvider;
|
|
|
|
|
import com.k12study.common.security.jwt.JwtUserPrincipal;
|
|
|
|
|
import com.k12study.common.web.exception.BizException;
|
|
|
|
|
import java.sql.ResultSet;
|
|
|
|
|
import java.sql.SQLException;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Optional;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import org.springframework.jdbc.core.RowMapper;
|
|
|
|
|
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
|
|
|
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
|
|
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
|
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import org.springframework.util.StringUtils;
|
|
|
|
|
|
|
|
|
|
@Service
|
|
|
|
|
public class AuthService {
|
|
|
|
|
private final JwtTokenProvider jwtTokenProvider;
|
|
|
|
|
private final NamedParameterJdbcTemplate jdbcTemplate;
|
|
|
|
|
private final AuthProperties authProperties;
|
|
|
|
|
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
|
|
|
|
|
|
|
|
|
public AuthService(JwtTokenProvider jwtTokenProvider) {
|
|
|
|
|
private static final RowMapper<UserRecord> USER_ROW_MAPPER = (rs, rowNum) -> mapUser(rs);
|
|
|
|
|
|
|
|
|
|
public AuthService(
|
|
|
|
|
JwtTokenProvider jwtTokenProvider,
|
|
|
|
|
NamedParameterJdbcTemplate jdbcTemplate,
|
|
|
|
|
AuthProperties authProperties) {
|
|
|
|
|
this.jwtTokenProvider = jwtTokenProvider;
|
|
|
|
|
this.jdbcTemplate = jdbcTemplate;
|
|
|
|
|
this.authProperties = authProperties;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public TokenResponse login(LoginRequest request) {
|
|
|
|
|
String username = request.username() == null || request.username().isBlank() ? "admin" : request.username();
|
|
|
|
|
JwtUserPrincipal principal = new JwtUserPrincipal(
|
|
|
|
|
"U10001",
|
|
|
|
|
username,
|
|
|
|
|
"K12Study 管理员",
|
|
|
|
|
request.tenantId() == null || request.tenantId().isBlank() ? "SCH-HQ" : request.tenantId(),
|
|
|
|
|
"DEPT-HQ-ADMIN"
|
|
|
|
|
);
|
|
|
|
|
String clientType = normalizeClientType(request == null ? null : request.clientType());
|
|
|
|
|
String tenantId = request == null || request.tenantId() == null ? "" : request.tenantId().trim();
|
|
|
|
|
UserRecord user = resolveLoginUser(request, tenantId);
|
|
|
|
|
verifyCredential(request, user);
|
|
|
|
|
|
|
|
|
|
List<String> roleCodes = findRoleCodes(user.userId());
|
|
|
|
|
ensureRoleAssigned(roleCodes);
|
|
|
|
|
validateClientRole(clientType, roleCodes);
|
|
|
|
|
|
|
|
|
|
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
JwtUserPrincipal principal = toPrincipal(user, roleCodes, clientType, sessionId);
|
|
|
|
|
String accessToken = jwtTokenProvider.createAccessToken(principal);
|
|
|
|
|
String refreshToken = jwtTokenProvider.createAccessToken(principal);
|
|
|
|
|
return new TokenResponse(accessToken, refreshToken, "Bearer", 12 * 60 * 60);
|
|
|
|
|
String refreshToken = jwtTokenProvider.createRefreshToken(principal);
|
|
|
|
|
saveRefreshToken(principal, refreshToken);
|
|
|
|
|
if ("MINI".equals(clientType)) {
|
|
|
|
|
enforceMiniSessionLimit(user.userId());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auditLogin(user, clientType, "SUCCESS", null);
|
|
|
|
|
return new TokenResponse(accessToken, refreshToken, "Bearer", authProperties.getAccessTokenTtl().toSeconds());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public TokenResponse refresh(String refreshToken) {
|
|
|
|
|
JwtUserPrincipal principal = jwtTokenProvider.parse(refreshToken);
|
|
|
|
|
TokenRecord tokenRecord = findTokenRecord(refreshToken)
|
|
|
|
|
.orElseThrow(() -> new BizException(401, "refreshToken 无效或已失效"));
|
|
|
|
|
if (tokenRecord.revoked() || tokenRecord.expireAt().isBefore(Instant.now())) {
|
|
|
|
|
throw new BizException(401, "refreshToken 已失效");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JwtUserPrincipal tokenPrincipal;
|
|
|
|
|
try {
|
|
|
|
|
tokenPrincipal = jwtTokenProvider.parse(refreshToken);
|
|
|
|
|
} catch (Exception exception) {
|
|
|
|
|
throw new BizException(401, "refreshToken 已失效");
|
|
|
|
|
}
|
|
|
|
|
if (!tokenRecord.userId().equals(tokenPrincipal.userId())
|
|
|
|
|
|| !tokenRecord.sessionId().equals(tokenPrincipal.sessionId())) {
|
|
|
|
|
throw new BizException(401, "refreshToken 校验失败");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UserRecord user = findUserById(tokenRecord.userId())
|
|
|
|
|
.orElseThrow(() -> new BizException(401, "用户不存在或已禁用"));
|
|
|
|
|
List<String> roleCodes = findRoleCodes(user.userId());
|
|
|
|
|
ensureRoleAssigned(roleCodes);
|
|
|
|
|
validateClientRole(tokenRecord.clientType(), roleCodes);
|
|
|
|
|
|
|
|
|
|
JwtUserPrincipal principal = toPrincipal(user, roleCodes, tokenRecord.clientType(), tokenRecord.sessionId());
|
|
|
|
|
String accessToken = jwtTokenProvider.createAccessToken(principal);
|
|
|
|
|
return new TokenResponse(accessToken, refreshToken, "Bearer", 12 * 60 * 60);
|
|
|
|
|
String nextRefreshToken = jwtTokenProvider.createRefreshToken(principal);
|
|
|
|
|
|
|
|
|
|
revokeToken(tokenRecord.tokenId());
|
|
|
|
|
saveRefreshToken(principal, nextRefreshToken);
|
|
|
|
|
|
|
|
|
|
auditLogin(user, tokenRecord.clientType(), "REFRESH_SUCCESS", null);
|
|
|
|
|
return new TokenResponse(accessToken, nextRefreshToken, "Bearer", authProperties.getAccessTokenTtl().toSeconds());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public CurrentUserResponse currentUser(String authorizationHeader) {
|
|
|
|
|
JwtUserPrincipal principal;
|
|
|
|
|
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
|
|
|
|
JwtUserPrincipal principal = jwtTokenProvider.parse(authorizationHeader.substring("Bearer ".length()));
|
|
|
|
|
return new CurrentUserResponse(
|
|
|
|
|
principal.userId(),
|
|
|
|
|
principal.username(),
|
|
|
|
|
principal.displayName(),
|
|
|
|
|
"330000",
|
|
|
|
|
"330100",
|
|
|
|
|
principal.tenantId(),
|
|
|
|
|
principal.deptId(),
|
|
|
|
|
List.of("SUPER_ADMIN", "ORG_ADMIN")
|
|
|
|
|
principal = jwtTokenProvider.parse(authorizationHeader.substring("Bearer ".length()));
|
|
|
|
|
} else {
|
|
|
|
|
var context = RequestUserContextHolder.get();
|
|
|
|
|
if (context == null || !StringUtils.hasText(context.userId())) {
|
|
|
|
|
throw new BizException(401, "未登录或登录已失效");
|
|
|
|
|
}
|
|
|
|
|
principal = new JwtUserPrincipal(
|
|
|
|
|
context.userId(),
|
|
|
|
|
context.username(),
|
|
|
|
|
context.displayName(),
|
|
|
|
|
context.adcode(),
|
|
|
|
|
context.tenantId(),
|
|
|
|
|
context.tenantPath(),
|
|
|
|
|
context.deptId(),
|
|
|
|
|
context.deptPath(),
|
|
|
|
|
context.roleCodes(),
|
|
|
|
|
normalizeClientType(context.clientType()),
|
|
|
|
|
context.sessionId()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
var context = RequestUserContextHolder.get();
|
|
|
|
|
|
|
|
|
|
UserRecord user = findUserById(principal.userId())
|
|
|
|
|
.orElseThrow(() -> new BizException(401, "用户不存在或已禁用"));
|
|
|
|
|
List<String> roleCodes = findRoleCodes(user.userId());
|
|
|
|
|
|
|
|
|
|
String areaCode = safeAdcode(user.adcode());
|
|
|
|
|
String provinceCode = areaCode.length() >= 2 ? areaCode.substring(0, 2) + "0000" : areaCode;
|
|
|
|
|
return new CurrentUserResponse(
|
|
|
|
|
context == null ? "U10001" : context.userId(),
|
|
|
|
|
context == null ? "admin" : context.username(),
|
|
|
|
|
context == null ? "K12Study 管理员" : context.displayName(),
|
|
|
|
|
"330000",
|
|
|
|
|
"330100",
|
|
|
|
|
context == null ? "SCH-HQ" : context.tenantId(),
|
|
|
|
|
context == null ? "DEPT-HQ-ADMIN" : context.deptId(),
|
|
|
|
|
List.of("SUPER_ADMIN", "ORG_ADMIN")
|
|
|
|
|
user.userId(),
|
|
|
|
|
user.username(),
|
|
|
|
|
user.displayName(),
|
|
|
|
|
provinceCode,
|
|
|
|
|
areaCode,
|
|
|
|
|
user.tenantId(),
|
|
|
|
|
user.tenantPath(),
|
|
|
|
|
user.deptId(),
|
|
|
|
|
user.deptPath(),
|
|
|
|
|
roleCodes,
|
|
|
|
|
principal.clientType()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private UserRecord resolveLoginUser(LoginRequest request, String tenantId) {
|
|
|
|
|
if (request == null) {
|
|
|
|
|
throw new BizException(400, "登录参数不能为空");
|
|
|
|
|
}
|
|
|
|
|
if (StringUtils.hasText(request.mobile())) {
|
|
|
|
|
return findUserByMobile(request.mobile().trim(), tenantId)
|
|
|
|
|
.orElseThrow(() -> {
|
|
|
|
|
auditLogin(null, normalizeClientType(request.clientType()), "FAILED", "MOBILE_NOT_FOUND");
|
|
|
|
|
return new BizException(401, "手机号或密码错误");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (!StringUtils.hasText(request.username())) {
|
|
|
|
|
throw new BizException(400, "用户名不能为空");
|
|
|
|
|
}
|
|
|
|
|
return findUserByUsername(request.username().trim(), tenantId)
|
|
|
|
|
.orElseThrow(() -> {
|
|
|
|
|
auditLogin(null, normalizeClientType(request.clientType()), "FAILED", "USER_NOT_FOUND");
|
|
|
|
|
return new BizException(401, "用户名或密码错误");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void verifyCredential(LoginRequest request, UserRecord user) {
|
|
|
|
|
boolean passwordPassed = StringUtils.hasText(request.password()) && passwordMatches(request.password(), user.passwordHash());
|
|
|
|
|
boolean smsPassed = StringUtils.hasText(request.mobile()) && StringUtils.hasText(request.smsCode()) && "123456".equals(request.smsCode());
|
|
|
|
|
if (!passwordPassed && !smsPassed) {
|
|
|
|
|
auditLogin(user, normalizeClientType(request.clientType()), "FAILED", "BAD_CREDENTIAL");
|
|
|
|
|
throw new BizException(401, "用户名/手机号或凭据错误");
|
|
|
|
|
}
|
|
|
|
|
if (!"ACTIVE".equalsIgnoreCase(user.status())) {
|
|
|
|
|
auditLogin(user, normalizeClientType(request.clientType()), "FAILED", "USER_DISABLED");
|
|
|
|
|
throw new BizException(403, "用户状态不可用");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void validateClientRole(String clientType, List<String> roleCodes) {
|
|
|
|
|
if ("MINI".equals(clientType) && roleCodes.stream().noneMatch("STUDENT"::equalsIgnoreCase)) {
|
|
|
|
|
throw new BizException(403, "小程序端仅允许学生账号登录");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ensureRoleAssigned(List<String> roleCodes) {
|
|
|
|
|
if (roleCodes == null || roleCodes.isEmpty()) {
|
|
|
|
|
throw new BizException(403, "用户未分配角色");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean passwordMatches(String rawPassword, String storedPassword) {
|
|
|
|
|
if (!StringUtils.hasText(storedPassword)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (storedPassword.startsWith("$2a$") || storedPassword.startsWith("$2b$") || storedPassword.startsWith("$2y$")) {
|
|
|
|
|
return passwordEncoder.matches(rawPassword, storedPassword);
|
|
|
|
|
}
|
|
|
|
|
return storedPassword.equals(rawPassword);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private JwtUserPrincipal toPrincipal(UserRecord user, List<String> roleCodes, String clientType, String sessionId) {
|
|
|
|
|
return new JwtUserPrincipal(
|
|
|
|
|
user.userId(),
|
|
|
|
|
user.username(),
|
|
|
|
|
user.displayName(),
|
|
|
|
|
user.adcode(),
|
|
|
|
|
user.tenantId(),
|
|
|
|
|
user.tenantPath(),
|
|
|
|
|
user.deptId(),
|
|
|
|
|
user.deptPath(),
|
|
|
|
|
roleCodes,
|
|
|
|
|
clientType,
|
|
|
|
|
sessionId
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<UserRecord> findUserByUsername(String username, String tenantId) {
|
|
|
|
|
String sql = """
|
|
|
|
|
SELECT user_id, username, display_name, password_hash, mobile_phone, adcode, tenant_id, tenant_path, dept_id, dept_path, status
|
|
|
|
|
FROM upms.tb_sys_user
|
|
|
|
|
WHERE username = :username
|
|
|
|
|
AND (:tenantId = '' OR tenant_id = :tenantId)
|
|
|
|
|
LIMIT 1
|
|
|
|
|
""";
|
|
|
|
|
return jdbcTemplate.query(sql, Map.of("username", username, "tenantId", tenantId), USER_ROW_MAPPER).stream().findFirst();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<UserRecord> findUserByMobile(String mobile, String tenantId) {
|
|
|
|
|
String sql = """
|
|
|
|
|
SELECT user_id, username, display_name, password_hash, mobile_phone, adcode, tenant_id, tenant_path, dept_id, dept_path, status
|
|
|
|
|
FROM upms.tb_sys_user
|
|
|
|
|
WHERE mobile_phone = :mobile
|
|
|
|
|
AND (:tenantId = '' OR tenant_id = :tenantId)
|
|
|
|
|
LIMIT 1
|
|
|
|
|
""";
|
|
|
|
|
return jdbcTemplate.query(sql, Map.of("mobile", mobile, "tenantId", tenantId), USER_ROW_MAPPER).stream().findFirst();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<UserRecord> findUserById(String userId) {
|
|
|
|
|
String sql = """
|
|
|
|
|
SELECT user_id, username, display_name, password_hash, mobile_phone, adcode, tenant_id, tenant_path, dept_id, dept_path, status
|
|
|
|
|
FROM upms.tb_sys_user
|
|
|
|
|
WHERE user_id = :userId
|
|
|
|
|
LIMIT 1
|
|
|
|
|
""";
|
|
|
|
|
return jdbcTemplate.query(sql, Map.of("userId", userId), USER_ROW_MAPPER).stream().findFirst();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private List<String> findRoleCodes(String userId) {
|
|
|
|
|
String sql = """
|
|
|
|
|
SELECT r.role_code
|
|
|
|
|
FROM upms.tb_sys_user_role ur
|
|
|
|
|
JOIN upms.tb_sys_role r ON r.role_id = ur.role_id
|
|
|
|
|
WHERE ur.user_id = :userId
|
|
|
|
|
ORDER BY r.role_code
|
|
|
|
|
""";
|
|
|
|
|
return jdbcTemplate.queryForList(sql, Map.of("userId", userId), String.class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<TokenRecord> findTokenRecord(String refreshToken) {
|
|
|
|
|
String sql = """
|
|
|
|
|
SELECT token_id, session_id, client_type, user_id, refresh_token, expire_at, revoked
|
|
|
|
|
FROM auth.tb_auth_refresh_token
|
|
|
|
|
WHERE refresh_token = :refreshToken
|
|
|
|
|
LIMIT 1
|
|
|
|
|
""";
|
|
|
|
|
RowMapper<TokenRecord> mapper = (rs, rowNum) -> new TokenRecord(
|
|
|
|
|
rs.getString("token_id"),
|
|
|
|
|
rs.getString("session_id"),
|
|
|
|
|
normalizeClientType(rs.getString("client_type")),
|
|
|
|
|
rs.getString("user_id"),
|
|
|
|
|
rs.getString("refresh_token"),
|
|
|
|
|
rs.getTimestamp("expire_at").toInstant(),
|
|
|
|
|
rs.getBoolean("revoked")
|
|
|
|
|
);
|
|
|
|
|
return jdbcTemplate.query(sql, Map.of("refreshToken", refreshToken), mapper).stream().findFirst();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveRefreshToken(JwtUserPrincipal principal, String refreshToken) {
|
|
|
|
|
String sql = """
|
|
|
|
|
INSERT INTO auth.tb_auth_refresh_token (
|
|
|
|
|
token_id, session_id, client_type, user_id, username, adcode,
|
|
|
|
|
tenant_id, tenant_path, dept_id, dept_path,
|
|
|
|
|
refresh_token, expire_at, revoked, revoked_at, last_active_at, created_at
|
|
|
|
|
) VALUES (
|
|
|
|
|
:tokenId, :sessionId, :clientType, :userId, :username, :adcode,
|
|
|
|
|
:tenantId, :tenantPath, :deptId, :deptPath,
|
|
|
|
|
:refreshToken, :expireAt, false, null, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
|
|
|
)
|
|
|
|
|
""";
|
|
|
|
|
MapSqlParameterSource parameters = new MapSqlParameterSource()
|
|
|
|
|
.addValue("tokenId", UUID.randomUUID().toString().replace("-", ""))
|
|
|
|
|
.addValue("sessionId", principal.sessionId())
|
|
|
|
|
.addValue("clientType", normalizeClientType(principal.clientType()))
|
|
|
|
|
.addValue("userId", principal.userId())
|
|
|
|
|
.addValue("username", principal.username())
|
|
|
|
|
.addValue("adcode", principal.adcode())
|
|
|
|
|
.addValue("tenantId", principal.tenantId())
|
|
|
|
|
.addValue("tenantPath", principal.tenantPath())
|
|
|
|
|
.addValue("deptId", principal.deptId())
|
|
|
|
|
.addValue("deptPath", principal.deptPath())
|
|
|
|
|
.addValue("refreshToken", refreshToken)
|
|
|
|
|
.addValue("expireAt", Instant.now().plus(authProperties.getRefreshTokenTtl()));
|
|
|
|
|
jdbcTemplate.update(sql, parameters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void revokeToken(String tokenId) {
|
|
|
|
|
String sql = """
|
|
|
|
|
UPDATE auth.tb_auth_refresh_token
|
|
|
|
|
SET revoked = true, revoked_at = CURRENT_TIMESTAMP
|
|
|
|
|
WHERE token_id = :tokenId
|
|
|
|
|
""";
|
|
|
|
|
jdbcTemplate.update(sql, Map.of("tokenId", tokenId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void enforceMiniSessionLimit(String userId) {
|
|
|
|
|
String sessionSql = """
|
|
|
|
|
SELECT session_id
|
|
|
|
|
FROM auth.tb_auth_refresh_token
|
|
|
|
|
WHERE user_id = :userId
|
|
|
|
|
AND client_type = 'MINI'
|
|
|
|
|
AND revoked = false
|
|
|
|
|
AND expire_at > CURRENT_TIMESTAMP
|
|
|
|
|
GROUP BY session_id
|
|
|
|
|
ORDER BY MAX(last_active_at) DESC
|
|
|
|
|
""";
|
|
|
|
|
List<String> sessions = jdbcTemplate.queryForList(sessionSql, Map.of("userId", userId), String.class);
|
|
|
|
|
if (sessions.size() <= 3) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
List<String> needRevoke = sessions.subList(3, sessions.size());
|
|
|
|
|
String revokeSql = """
|
|
|
|
|
UPDATE auth.tb_auth_refresh_token
|
|
|
|
|
SET revoked = true, revoked_at = CURRENT_TIMESTAMP
|
|
|
|
|
WHERE user_id = :userId
|
|
|
|
|
AND session_id IN (:sessionIds)
|
|
|
|
|
AND revoked = false
|
|
|
|
|
""";
|
|
|
|
|
jdbcTemplate.update(revokeSql, new MapSqlParameterSource()
|
|
|
|
|
.addValue("userId", userId)
|
|
|
|
|
.addValue("sessionIds", needRevoke));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void auditLogin(UserRecord user, String clientType, String loginStatus, String failureReason) {
|
|
|
|
|
String sql = """
|
|
|
|
|
INSERT INTO auth.tb_auth_login_audit (
|
|
|
|
|
audit_id, user_id, username, client_type, adcode, tenant_id, tenant_path,
|
|
|
|
|
dept_id, dept_path, login_ip, login_status, failure_reason, created_at
|
|
|
|
|
) VALUES (
|
|
|
|
|
:auditId, :userId, :username, :clientType, :adcode, :tenantId, :tenantPath,
|
|
|
|
|
:deptId, :deptPath, :loginIp, :loginStatus, :failureReason, CURRENT_TIMESTAMP
|
|
|
|
|
)
|
|
|
|
|
""";
|
|
|
|
|
MapSqlParameterSource parameters = new MapSqlParameterSource()
|
|
|
|
|
.addValue("auditId", UUID.randomUUID().toString().replace("-", ""))
|
|
|
|
|
.addValue("userId", user == null ? null : user.userId())
|
|
|
|
|
.addValue("username", user == null ? "UNKNOWN" : user.username())
|
|
|
|
|
.addValue("clientType", normalizeClientType(clientType))
|
|
|
|
|
.addValue("adcode", user == null ? null : user.adcode())
|
|
|
|
|
.addValue("tenantId", user == null ? null : user.tenantId())
|
|
|
|
|
.addValue("tenantPath", user == null ? null : user.tenantPath())
|
|
|
|
|
.addValue("deptId", user == null ? null : user.deptId())
|
|
|
|
|
.addValue("deptPath", user == null ? null : user.deptPath())
|
|
|
|
|
.addValue("loginIp", null)
|
|
|
|
|
.addValue("loginStatus", loginStatus)
|
|
|
|
|
.addValue("failureReason", failureReason);
|
|
|
|
|
jdbcTemplate.update(sql, parameters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String normalizeClientType(String clientType) {
|
|
|
|
|
if (!StringUtils.hasText(clientType)) {
|
|
|
|
|
return "WEB";
|
|
|
|
|
}
|
|
|
|
|
String normalized = clientType.trim().toUpperCase();
|
|
|
|
|
return "MINI".equals(normalized) ? "MINI" : "WEB";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static UserRecord mapUser(ResultSet rs) throws SQLException {
|
|
|
|
|
return new UserRecord(
|
|
|
|
|
rs.getString("user_id"),
|
|
|
|
|
rs.getString("username"),
|
|
|
|
|
rs.getString("display_name"),
|
|
|
|
|
rs.getString("password_hash"),
|
|
|
|
|
rs.getString("mobile_phone"),
|
|
|
|
|
rs.getString("adcode"),
|
|
|
|
|
rs.getString("tenant_id"),
|
|
|
|
|
rs.getString("tenant_path"),
|
|
|
|
|
rs.getString("dept_id"),
|
|
|
|
|
rs.getString("dept_path"),
|
|
|
|
|
rs.getString("status")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String safeAdcode(String adcode) {
|
|
|
|
|
return StringUtils.hasText(adcode) ? adcode : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private record UserRecord(
|
|
|
|
|
String userId,
|
|
|
|
|
String username,
|
|
|
|
|
String displayName,
|
|
|
|
|
String passwordHash,
|
|
|
|
|
String mobilePhone,
|
|
|
|
|
String adcode,
|
|
|
|
|
String tenantId,
|
|
|
|
|
String tenantPath,
|
|
|
|
|
String deptId,
|
|
|
|
|
String deptPath,
|
|
|
|
|
String status
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private record TokenRecord(
|
|
|
|
|
String tokenId,
|
|
|
|
|
String sessionId,
|
|
|
|
|
String clientType,
|
|
|
|
|
String userId,
|
|
|
|
|
String refreshToken,
|
|
|
|
|
Instant expireAt,
|
|
|
|
|
boolean revoked
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|