forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
119 lines
4.7 KiB
119 lines
4.7 KiB
package com.mydreamjob.auth;
|
|
|
|
import org.springframework.data.jpa.repository.JpaRepository;
|
|
import org.springframework.data.jpa.repository.Modifying;
|
|
import org.springframework.data.jpa.repository.Query;
|
|
import org.springframework.data.repository.query.Param;
|
|
import org.springframework.stereotype.Repository;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
/**
|
|
* auth_tokens 数据访问(S02)。
|
|
*
|
|
* <p>并发消费与撤销**一律通过带条件的原子 UPDATE** 完成,并严格检查受影响行数;
|
|
* 禁止以 {@code findById -> set -> save} 作为并发保证。</p>
|
|
*/
|
|
@Repository
|
|
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
|
|
|
|
List<AuthToken> findByUserIdAndRevokedFalse(Long userId);
|
|
|
|
Optional<AuthToken> findByTokenHash(String tokenHash);
|
|
|
|
Optional<AuthToken> findByRefreshTokenHash(String refreshTokenHash);
|
|
|
|
List<AuthToken> findByExpiredAtBefore(LocalDateTime expiredAt);
|
|
|
|
// ── S02 会话查询 ────────────────────────────────────────────
|
|
|
|
/**
|
|
* 按摘要取候选 Refresh 记录(同摘要理论上唯一,取最新一条以防历史重复)。
|
|
*/
|
|
Optional<AuthToken> findTopByTokenHashOrderByIdDesc(String tokenHash);
|
|
|
|
/**
|
|
* 会话是否活动:同一 user + session 下存在未消费、未撤销且未过期的 Refresh 记录。
|
|
*
|
|
* <p>旧数据 {@code sessionId IS NULL} 永远不会命中。</p>
|
|
*/
|
|
@Query("""
|
|
SELECT COUNT(t) FROM AuthToken t
|
|
WHERE t.userId = :userId
|
|
AND t.sessionId = :sessionId
|
|
AND t.revoked = false
|
|
AND t.consumedAt IS NULL
|
|
AND t.expiredAt > :capturedNow
|
|
""")
|
|
long countActiveSessionTokens(@Param("userId") Long userId,
|
|
@Param("sessionId") String sessionId,
|
|
@Param("capturedNow") LocalDateTime capturedNow);
|
|
|
|
List<AuthToken> findByUserIdAndSessionIdOrderByIdAsc(Long userId, String sessionId);
|
|
|
|
// ── S02 原子条件更新(返回值必须被严格检查)────────────────────
|
|
|
|
/**
|
|
* 原子消费一条 Refresh 记录。只有满足以下全部条件才会成功:
|
|
* id、expectedVersion、未撤销、未消费、未过期。
|
|
*
|
|
* @return 受影响行数;0 表示并发冲突、已被消费、已被撤销或已过期
|
|
*/
|
|
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
|
@Query("""
|
|
UPDATE AuthToken t
|
|
SET t.consumedAt = :consumedAt,
|
|
t.version = t.version + 1
|
|
WHERE t.id = :id
|
|
AND t.version = :expectedVersion
|
|
AND t.revoked = false
|
|
AND t.consumedAt IS NULL
|
|
AND t.expiredAt > :capturedNow
|
|
""")
|
|
int consumeIfCurrent(@Param("id") Long id,
|
|
@Param("expectedVersion") Long expectedVersion,
|
|
@Param("consumedAt") LocalDateTime consumedAt,
|
|
@Param("capturedNow") LocalDateTime capturedNow);
|
|
|
|
/**
|
|
* 撤销某用户某个会话下所有未撤销记录。
|
|
*
|
|
* @return 受影响行数
|
|
*/
|
|
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
|
@Query("""
|
|
UPDATE AuthToken t
|
|
SET t.revoked = true,
|
|
t.revokedAt = :revokedAt,
|
|
t.revocationReason = :reason,
|
|
t.version = t.version + 1
|
|
WHERE t.userId = :userId
|
|
AND t.sessionId = :sessionId
|
|
AND t.revoked = false
|
|
""")
|
|
int revokeSession(@Param("userId") Long userId,
|
|
@Param("sessionId") String sessionId,
|
|
@Param("revokedAt") LocalDateTime revokedAt,
|
|
@Param("reason") String reason);
|
|
|
|
/**
|
|
* 撤销某用户全部未撤销记录(账号注销完成、用户禁用等全局安全事件)。
|
|
*
|
|
* @return 受影响行数
|
|
*/
|
|
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
|
@Query("""
|
|
UPDATE AuthToken t
|
|
SET t.revoked = true,
|
|
t.revokedAt = :revokedAt,
|
|
t.revocationReason = :reason,
|
|
t.version = t.version + 1
|
|
WHERE t.userId = :userId
|
|
AND t.revoked = false
|
|
""")
|
|
int revokeAllForUser(@Param("userId") Long userId,
|
|
@Param("revokedAt") LocalDateTime revokedAt,
|
|
@Param("reason") String reason);
|
|
}
|
|
|