优化
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.auth.client;
|
||||
|
||||
import feign.Request;
|
||||
import feign.Retryer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Only the IAM authorization client uses this bounded, single-attempt chain. */
|
||||
public class AuthorizationFeignConfiguration {
|
||||
@Bean public Request.Options authorizationRequestOptions() {
|
||||
return new Request.Options(1, TimeUnit.SECONDS, 2, TimeUnit.SECONDS, false);
|
||||
}
|
||||
@Bean public Retryer authorizationRetryer() { return Retryer.NEVER_RETRY; }
|
||||
}
|
||||
@@ -23,6 +23,14 @@ public class AuthorizationSnapshot {
|
||||
private Long permissionVersion;
|
||||
private Long overseasWarehouseId;
|
||||
private Long domesticWarehouseId;
|
||||
private Long tenantVersion;
|
||||
|
||||
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions, Long permissionVersion,
|
||||
Long overseasWarehouseId, Long domesticWarehouseId) {
|
||||
this(userId, tenantId, membershipId, appCode, clientType, roles, permissions,
|
||||
permissionVersion, overseasWarehouseId, domesticWarehouseId, 0L);
|
||||
}
|
||||
|
||||
public AuthorizationSnapshot(Long userId, Long tenantId, Long membershipId, String appCode,
|
||||
String clientType, Set<String> roles, Set<String> permissions,
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
/**
|
||||
* 认证服务访问 IAM 内部授权接口的声明式客户端,用于加载用户在指定应用和租户下的权限快照。
|
||||
*/
|
||||
@FeignClient(name = "iam-service", contextId = "iamAuthorizationClient",
|
||||
@FeignClient(name = "iam-service", contextId = "iamAuthorizationClient", configuration = AuthorizationFeignConfiguration.class,
|
||||
url = "${baiyue.clients.iam.url:}", path = "/internal/iam")
|
||||
public interface IamAuthorizationClient {
|
||||
@PostMapping("/authorization-snapshot")
|
||||
|
||||
@@ -3,15 +3,12 @@ package com.baiyue.auth.controller;
|
||||
import com.baiyue.auth.dto.AccountManagementDtos.*;
|
||||
import com.baiyue.auth.service.AccountManagementService;
|
||||
import com.baiyue.auth.service.AuthService;
|
||||
import com.baiyue.auth.service.OverseasSessionValidationService;
|
||||
import com.baiyue.auth.dto.OverseasSessionValidationDtos;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -22,14 +19,13 @@ import java.security.MessageDigest;
|
||||
/** 仅供可信内部服务调用的账号管理接口。 */
|
||||
@RestController
|
||||
@RequestMapping("/internal/auth/accounts")
|
||||
@Tag(name = "Internal Auth / 内部认证", description = "Service-token protected internal account and overseas PDA session APIs")
|
||||
@Tag(name = "Internal Auth / 内部认证", description = "Service-token protected internal account and unified session APIs")
|
||||
public class InternalAccountController {
|
||||
@Resource
|
||||
private AccountManagementService service;
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
@Resource
|
||||
private OverseasSessionValidationService overseasSessionValidationService;
|
||||
@Resource private com.baiyue.auth.service.SessionStateService sessionStateService;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String configuredToken;
|
||||
@@ -71,15 +67,17 @@ public class InternalAccountController {
|
||||
@Valid @RequestBody RevokeAccountSessionsRequest request) {
|
||||
verify(token); service.revokeSessions(request.accountIds()); return ApiResponse.success(null);
|
||||
}
|
||||
@PostMapping("/overseas-session/validate")
|
||||
@Operation(summary = "Validate verified overseas PDA claims / 校验已验签的海外PDA声明",
|
||||
description = "Accepts only claims extracted after Gateway JWT verification; raw tokens and user credentials are forbidden.")
|
||||
public ApiResponse<OverseasSessionValidationDtos.Response> validateOverseasSession(
|
||||
public record SessionValidationRequest(@jakarta.validation.constraints.NotBlank String jwtId,
|
||||
@jakarta.validation.constraints.NotNull Long userId) { }
|
||||
|
||||
@PostMapping("/sessions/validate")
|
||||
public ApiResponse<Boolean> validateSession(
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||
@Valid @RequestBody OverseasSessionValidationDtos.Request request) {
|
||||
@Valid @RequestBody SessionValidationRequest request) {
|
||||
verify(token);
|
||||
return ApiResponse.success(overseasSessionValidationService.validate(request));
|
||||
return ApiResponse.success(sessionStateService.validate(request.jwtId(), request.userId()));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/profile")
|
||||
public ApiResponse<Void> profile(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||
@PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.baiyue.auth.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/** 海外 PDA 已验签身份的实时会话校验契约;绝不接收 raw JWT 或用户凭据。 */
|
||||
public final class OverseasSessionValidationDtos {
|
||||
private OverseasSessionValidationDtos() { }
|
||||
|
||||
@Schema(name = "OverseasSessionValidationRequest",
|
||||
description = "Claims extracted only after overseas Gateway verifies the JWT signature")
|
||||
public record Request(
|
||||
@NotNull Long userId,
|
||||
@NotNull Integer tokenVersion,
|
||||
Long overseasWarehouseId) { }
|
||||
|
||||
@Schema(name = "OverseasSessionValidationResponse")
|
||||
public record Response(boolean valid, FailureReason failureReason) {
|
||||
public static Response accepted() { return new Response(true, null); }
|
||||
public static Response rejected(FailureReason reason) { return new Response(false, reason); }
|
||||
}
|
||||
|
||||
public enum FailureReason {
|
||||
ACCOUNT_INVALID,
|
||||
TOKEN_VERSION_MISMATCH,
|
||||
WAREHOUSE_UNBOUND,
|
||||
WAREHOUSE_MISMATCH
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ public class AuthAccountRepository {
|
||||
public AuthAccount findByUsername(String username) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>()
|
||||
.eq(AuthAccount::getUsername, username)
|
||||
.last("LIMIT 1"));
|
||||
.last("LIMIT 1 FOR UPDATE"));
|
||||
}
|
||||
|
||||
public AuthAccount findByPhone(String phone) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>()
|
||||
.eq(AuthAccount::getPhone, phone)
|
||||
.last("LIMIT 1"));
|
||||
.last("LIMIT 1 FOR UPDATE"));
|
||||
}
|
||||
|
||||
public boolean existsByUsername(String username) {
|
||||
@@ -53,13 +53,15 @@ public class AuthAccountRepository {
|
||||
public void insert(AuthAccount account) { mapper.insert(account); }
|
||||
|
||||
public AuthAccount findById(Long id) {
|
||||
return mapper.selectById(id);
|
||||
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>().eq(AuthAccount::getId, id).last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public AuthAccount findCurrentById(Long id) { return mapper.selectById(id); }
|
||||
|
||||
public List<AuthAccount> findMerchantByTenant(Long tenantId) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<AuthAccount>()
|
||||
.eq(AuthAccount::getDefaultTenantId, tenantId)
|
||||
.eq(AuthAccount::getDefaultAppCode, "MERCHANT"));
|
||||
.eq(AuthAccount::getDefaultAppCode, "MERCHANT").orderByAsc(AuthAccount::getId).last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public void save(AuthAccount account) {
|
||||
|
||||
@@ -108,7 +108,7 @@ public class AccountManagementService {
|
||||
resourceId = "'RBAC_CHANGE'", description = "RBAC 变更批量撤销账号会话",
|
||||
captureBefore = false, captureAfter = false)
|
||||
public void revokeSessions(Set<Long> accountIds) {
|
||||
accountIds.forEach(accountId -> bumpVersion(required(accountId)));
|
||||
accountIds.stream().sorted().forEach(accountId -> bumpVersion(required(accountId)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -92,7 +92,8 @@ public class AuthService {
|
||||
AuthAccount account = accountRepository.findById(session.userId());
|
||||
if (account == null || !"ACTIVE".equals(account.getAccountStatus()))
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用");
|
||||
return issue(account);
|
||||
if (session.fingerprint() == null) throw new BizException(ResultCode.UNAUTHORIZED, "刷新会话缺少授权状态");
|
||||
return issue(account, session.fingerprint());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -109,8 +110,13 @@ public class AuthService {
|
||||
accountRepository.save(account);
|
||||
}
|
||||
|
||||
private TokenResponse issue(AuthAccount account) {
|
||||
private TokenResponse issue(AuthAccount account) { return issue(account, null); }
|
||||
|
||||
private TokenResponse issue(AuthAccount account, String expectedState) {
|
||||
AuthorizationSnapshot authorization = authorizationLoader.load(account);
|
||||
if (expectedState != null && !expectedState.equals(SessionStateService.fingerprint(account, authorization))) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "授权状态已变更,请重新登录");
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
String jwtId = UUID.randomUUID().toString();
|
||||
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
|
||||
@@ -145,7 +151,8 @@ public class AuthService {
|
||||
byte[] random = new byte[48];
|
||||
secureRandom.nextBytes(random);
|
||||
String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
|
||||
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(), rawRefresh,
|
||||
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(),
|
||||
SessionStateService.fingerprint(account, authorization), rawRefresh,
|
||||
properties.accessTokenTtl(), properties.refreshTokenTtl());
|
||||
return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(),
|
||||
rawRefresh, properties.refreshTokenTtl().toSeconds(),
|
||||
@@ -202,7 +209,7 @@ public class AuthService {
|
||||
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired())).build();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(signingHeader(), claims)).getTokenValue();
|
||||
tokenSessionStore.storeAccessOnly(jwtId, account.getId(), authorization.getRoles(),
|
||||
authorization.getPermissions(), Duration.ofMinutes(15));
|
||||
authorization.getPermissions(), SessionStateService.fingerprint(account, authorization), Duration.ofMinutes(15));
|
||||
return new ImpersonationTokenResponse("Bearer", accessToken, 900,
|
||||
authorization.getRoles(), authorization.getPermissions());
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.baiyue.auth.service;
|
||||
|
||||
import com.baiyue.auth.client.AuthorizationLoader;
|
||||
import com.baiyue.auth.dto.OverseasSessionValidationDtos.FailureReason;
|
||||
import com.baiyue.auth.dto.OverseasSessionValidationDtos.Request;
|
||||
import com.baiyue.auth.dto.OverseasSessionValidationDtos.Response;
|
||||
import com.baiyue.auth.entity.AuthAccount;
|
||||
import com.baiyue.auth.repository.AuthAccountRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** 实时核对海外 PDA token 的账号版本与当前 IAM 仓绑定。 */
|
||||
@Service
|
||||
public class OverseasSessionValidationService {
|
||||
@Resource private AuthAccountRepository accounts;
|
||||
@Resource private AuthorizationLoader authorizationLoader;
|
||||
|
||||
public Response validate(Request request) {
|
||||
AuthAccount account = accounts.findById(request.userId());
|
||||
if (account == null || !"ACTIVE".equals(account.getAccountStatus())) {
|
||||
return Response.rejected(FailureReason.ACCOUNT_INVALID);
|
||||
}
|
||||
int currentVersion = account.getTokenVersion() == null ? 0 : account.getTokenVersion();
|
||||
if (currentVersion != request.tokenVersion()) {
|
||||
return Response.rejected(FailureReason.TOKEN_VERSION_MISMATCH);
|
||||
}
|
||||
Long currentWarehouseId = authorizationLoader.load(account).getOverseasWarehouseId();
|
||||
if (currentWarehouseId == null || request.overseasWarehouseId() == null) {
|
||||
return Response.rejected(FailureReason.WAREHOUSE_UNBOUND);
|
||||
}
|
||||
if (!Objects.equals(currentWarehouseId, request.overseasWarehouseId())) {
|
||||
return Response.rejected(FailureReason.WAREHOUSE_MISMATCH);
|
||||
}
|
||||
return Response.accepted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.auth.service;
|
||||
|
||||
import com.baiyue.auth.client.AuthorizationLoader;
|
||||
import com.baiyue.auth.client.AuthorizationSnapshot;
|
||||
import com.baiyue.auth.entity.AuthAccount;
|
||||
import com.baiyue.auth.repository.AuthAccountRepository;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/** No cache: a validation started after a security commit must observe that commit. */
|
||||
@Service
|
||||
public class SessionStateService {
|
||||
private static final ObjectMapper FINGERPRINT_JSON = new ObjectMapper();
|
||||
@Resource private AuthAccountRepository accounts;
|
||||
@Resource private AuthorizationLoader authorizationLoader;
|
||||
@Resource private TokenSessionStore sessions;
|
||||
|
||||
public static String fingerprint(AuthAccount account, AuthorizationSnapshot snapshot) {
|
||||
try {
|
||||
if (account.getTokenVersion() == null || snapshot.getPermissionVersion() == null
|
||||
|| snapshot.getTenantVersion() == null) throw new IllegalArgumentException("Missing persisted security version");
|
||||
// Ordered JSON array avoids set ordering and delimiter ambiguities.
|
||||
Object[] state = {account.getId(), account.getTokenVersion(), account.getDefaultTenantId(),
|
||||
account.getDefaultMembershipId(), account.getDefaultAppCode(), account.getOperatorTenantId(),
|
||||
snapshot.getPermissionVersion(), snapshot.getTenantVersion(), snapshot.getClientType(),
|
||||
snapshot.getOverseasWarehouseId(), snapshot.getDomesticWarehouseId(),
|
||||
new TreeSet<>(snapshot.getRoles()), new TreeSet<>(snapshot.getPermissions())};
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(FINGERPRINT_JSON.writeValueAsBytes(state)));
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Cannot fingerprint authorization state", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCurrent(Long userId, String expected) {
|
||||
AuthAccount account = accounts.findCurrentById(userId);
|
||||
if (account == null || !"ACTIVE".equals(account.getAccountStatus())
|
||||
|| Integer.valueOf(1).equals(account.getDeleted()) || expected == null
|
||||
|| !expected.equals(fingerprint(account, authorizationLoader.load(account)))) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "登录授权已变更,请重新登录");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW,
|
||||
isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public boolean validate(String jwtId, Long userId) {
|
||||
TokenSessionStore.SessionState state = sessions.loadState(jwtId);
|
||||
if (state == null || !state.userId().equals(userId)) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "登录会话已失效");
|
||||
}
|
||||
requireCurrent(userId, state.fingerprint());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.SetOperations;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -21,8 +19,6 @@ import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证会话存储服务,使用 Redis 保存和一次性消费刷新会话,支持刷新令牌轮换及主动撤销。
|
||||
@@ -42,7 +38,7 @@ public class TokenSessionStore {
|
||||
}
|
||||
|
||||
public void store(String jwtId, Long userId, Set<String> roles, Set<String> permissions,
|
||||
String rawRefreshToken, Duration accessTtl, Duration refreshTtl) {
|
||||
String fingerprint, String rawRefreshToken, Duration accessTtl, Duration refreshTtl) {
|
||||
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
|
||||
String refreshKey = RedisTokenKeys.refresh(keyPrefix, sha256(rawRefreshToken));
|
||||
String userKey = userSessionsKey(userId);
|
||||
@@ -50,25 +46,27 @@ public class TokenSessionStore {
|
||||
String accessValue = objectMapper.writeValueAsString(
|
||||
new AccessSession(1, userId, normalized(roles), normalized(permissions)));
|
||||
redisTemplate.opsForValue().set(accessKey, accessValue, accessTtl);
|
||||
String refreshValue = objectMapper.writeValueAsString(new RefreshSession(userId, jwtId));
|
||||
storeState(jwtId, userId, fingerprint, accessTtl);
|
||||
String refreshValue = objectMapper.writeValueAsString(new RefreshSession(userId, jwtId, fingerprint));
|
||||
redisTemplate.opsForValue().set(refreshKey, refreshValue, refreshTtl);
|
||||
SetOperations<String, String> sets = redisTemplate.opsForSet();
|
||||
if (sets != null) sets.add(userKey, accessKey, refreshKey);
|
||||
if (sets != null) sets.add(userKey, accessKey, refreshKey, stateKey(jwtId));
|
||||
redisTemplate.expire(userKey, refreshTtl);
|
||||
} catch (Exception exception) {
|
||||
try { redisTemplate.delete(Set.of(accessKey, refreshKey, userKey)); } catch (DataAccessException ignored) { }
|
||||
try { redisTemplate.delete(Set.of(accessKey, refreshKey, stateKey(jwtId))); } catch (DataAccessException ignored) { }
|
||||
throw unavailable(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void storeAccessOnly(String jwtId, Long userId, Set<String> roles,
|
||||
Set<String> permissions, Duration ttl) {
|
||||
Set<String> permissions, String fingerprint, Duration ttl) {
|
||||
try {
|
||||
String accessKey = RedisTokenKeys.access(keyPrefix, jwtId);
|
||||
String accessValue = objectMapper.writeValueAsString(
|
||||
new AccessSession(1, userId, normalized(roles), normalized(permissions)));
|
||||
redisTemplate.opsForValue().set(accessKey, accessValue, ttl);
|
||||
redisTemplate.opsForSet().add(userSessionsKey(userId), accessKey);
|
||||
storeState(jwtId, userId, fingerprint, ttl);
|
||||
redisTemplate.opsForSet().add(userSessionsKey(userId), accessKey, stateKey(jwtId));
|
||||
redisTemplate.expire(userSessionsKey(userId), ttl);
|
||||
} catch (Exception exception) {
|
||||
throw unavailable(exception);
|
||||
@@ -103,6 +101,7 @@ public class TokenSessionStore {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
RefreshSession session = objectMapper.readValue(value, RefreshSession.class);
|
||||
redisTemplate.delete(RedisTokenKeys.access(keyPrefix, session.accessJwtId()));
|
||||
redisTemplate.delete(stateKey(session.accessJwtId()));
|
||||
return session;
|
||||
} catch (DataAccessException exception) {
|
||||
throw unavailable(exception);
|
||||
@@ -121,43 +120,26 @@ public class TokenSessionStore {
|
||||
try {
|
||||
Set<String> keys = redisTemplate.opsForSet().members(userKey);
|
||||
if (keys != null && !keys.isEmpty()) redisTemplate.delete(keys);
|
||||
// 兼容功能上线前尚未写入用户会话索引的存量令牌,SCAN 不阻塞 Redis 主线程。
|
||||
List<String> legacyKeys = new ArrayList<>();
|
||||
collectLegacyKeys(keyPrefix + ":access:*", userId, legacyKeys, false);
|
||||
collectLegacyKeys(keyPrefix + ":refresh:*", userId, legacyKeys, true);
|
||||
if (!legacyKeys.isEmpty()) redisTemplate.delete(legacyKeys);
|
||||
redisTemplate.delete(userKey);
|
||||
} catch (DataAccessException exception) {
|
||||
throw unavailable(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectLegacyKeys(String pattern, Long userId, List<String> result, boolean refresh) {
|
||||
Cursor<String> cursor = redisTemplate.scan(ScanOptions.scanOptions().match(pattern).count(500).build());
|
||||
if (cursor == null) return;
|
||||
try (cursor) {
|
||||
while (cursor.hasNext()) {
|
||||
String key = cursor.next();
|
||||
String value = redisTemplate.opsForValue().get(key);
|
||||
if ((!refresh && accessBelongsTo(value, userId)) || (refresh && belongsTo(value, userId))) {
|
||||
result.add(key);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw unavailable(exception);
|
||||
}
|
||||
private String stateKey(String jwtId) { return keyPrefix + ":state:" + jwtId; }
|
||||
|
||||
private void storeState(String jwtId, Long userId, String fingerprint, Duration ttl) throws Exception {
|
||||
if (fingerprint == null || fingerprint.isBlank()) throw new IllegalArgumentException("Missing security state");
|
||||
redisTemplate.opsForValue().set(stateKey(jwtId),
|
||||
objectMapper.writeValueAsString(new SessionState(userId, fingerprint)), ttl);
|
||||
}
|
||||
|
||||
private boolean belongsTo(String value, Long userId) {
|
||||
if (value == null || value.isBlank()) return false;
|
||||
try { return userId.equals(objectMapper.readValue(value, RefreshSession.class).userId()); }
|
||||
catch (Exception ignored) { return false; }
|
||||
}
|
||||
|
||||
private boolean accessBelongsTo(String value, Long userId) {
|
||||
if (value == null || value.isBlank()) return false;
|
||||
try { return userId.equals(objectMapper.readValue(value, AccessSession.class).userId()); }
|
||||
catch (Exception ignored) { return false; }
|
||||
public SessionState loadState(String jwtId) {
|
||||
try {
|
||||
if (!Boolean.TRUE.equals(redisTemplate.hasKey(RedisTokenKeys.access(keyPrefix, jwtId)))) return null;
|
||||
String value = redisTemplate.opsForValue().get(stateKey(jwtId));
|
||||
return value == null ? null : objectMapper.readValue(value, SessionState.class);
|
||||
} catch (Exception failure) { throw unavailable(failure); }
|
||||
}
|
||||
|
||||
private static Set<String> normalized(Set<String> values) {
|
||||
@@ -182,7 +164,8 @@ public class TokenSessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
public record RefreshSession(Long userId, String accessJwtId) { }
|
||||
public record SessionState(Long userId, String fingerprint) { }
|
||||
public record RefreshSession(Long userId, String accessJwtId, String fingerprint) { }
|
||||
public record AccessSession(int version, Long userId, Set<String> roles, Set<String> permissions) {
|
||||
public AccessSession {
|
||||
roles = normalized(roles);
|
||||
|
||||
Reference in New Issue
Block a user