@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
public class AppException extends RuntimeException {
|
||||
|
||||
public AppException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.AuditMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AuditService {
|
||||
|
||||
private final AuditMapper auditMapper;
|
||||
|
||||
public AuditService(AuditMapper auditMapper) {
|
||||
this.auditMapper = auditMapper;
|
||||
}
|
||||
|
||||
public void record(AuditEvent event) {
|
||||
auditMapper.insert(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.IssuedToken;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.TokenIssueCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.BearerTokenMapper;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import java.time.Clock;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class BearerTokenService {
|
||||
|
||||
private final BearerTokenMapper tokenMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final AuditService auditService;
|
||||
private final TokenGenerator tokenGenerator;
|
||||
private final TokenHasher tokenHasher;
|
||||
private final BackofficeProperties properties;
|
||||
private final Clock clock;
|
||||
|
||||
public BearerTokenService(
|
||||
BearerTokenMapper tokenMapper,
|
||||
UserMapper userMapper,
|
||||
AuditService auditService,
|
||||
TokenGenerator tokenGenerator,
|
||||
TokenHasher tokenHasher,
|
||||
BackofficeProperties properties,
|
||||
Clock clock
|
||||
) {
|
||||
this.tokenMapper = tokenMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.auditService = auditService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
this.tokenHasher = tokenHasher;
|
||||
this.properties = properties;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public List<BearerTokenRecord> findAll() {
|
||||
return tokenMapper.findAll();
|
||||
}
|
||||
|
||||
public BearerTokenRecord findById(long keyId) {
|
||||
return tokenMapper.findById(keyId);
|
||||
}
|
||||
|
||||
public boolean matches(BearerTokenRecord record, String plainToken) {
|
||||
if (record == null || plainToken == null || plainToken.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String hash = tokenHasher.sha256(plainToken);
|
||||
return hash.equalsIgnoreCase(record.keyHash());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public IssuedToken issueToken(TokenIssueCommand command) {
|
||||
AppUser user = userMapper.findById(command.userId());
|
||||
if (user == null) {
|
||||
throw new AppException("사용자를 찾을 수 없습니다.");
|
||||
}
|
||||
if (!user.active()) {
|
||||
throw new AppException("비활성 사용자에게는 토큰을 발급할 수 없습니다.");
|
||||
}
|
||||
|
||||
OffsetDateTime now = OffsetDateTime.now(clock);
|
||||
if (command.expiresAt() == null || !command.expiresAt().isAfter(now)) {
|
||||
throw new AppException("만료일은 현재 시각 이후여야 합니다.");
|
||||
}
|
||||
if (command.expiresAt().isAfter(now.plusDays(properties.token().maxDays()))) {
|
||||
throw new AppException("토큰 만료일이 최대 허용 기간을 초과했습니다.");
|
||||
}
|
||||
|
||||
String plainToken = tokenGenerator.generate();
|
||||
String prefix = tokenGenerator.prefix(plainToken);
|
||||
String hash = tokenHasher.sha256(plainToken);
|
||||
long keyId = tokenMapper.nextKeyId();
|
||||
|
||||
tokenMapper.insertToken(new BearerTokenRecord(
|
||||
keyId,
|
||||
user.userId(),
|
||||
user.username(),
|
||||
prefix,
|
||||
hash,
|
||||
command.expiresAt(),
|
||||
null,
|
||||
command.description()
|
||||
));
|
||||
auditService.record(new AuditEvent("TOKEN_ISSUED", keyId, null, "SUCCESS", null, null, "issued"));
|
||||
return new IssuedToken(keyId, prefix, plainToken, command.expiresAt());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void revokeToken(long keyId, String reason) {
|
||||
int updated = tokenMapper.revokeToken(keyId, OffsetDateTime.now(clock), reason);
|
||||
if (updated == 0) {
|
||||
throw new AppException("회수할 활성 토큰을 찾을 수 없습니다.");
|
||||
}
|
||||
auditService.record(new AuditEvent("TOKEN_REVOKED", keyId, null, "SUCCESS", null, null, reason));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URI;
|
||||
import java.time.Clock;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@Service
|
||||
public class OrdsProbeService {
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final BearerTokenService tokenService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final AuditService auditService;
|
||||
private final ProbeErrorClassifier errorClassifier;
|
||||
private final RestTemplate ordsRestTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BackofficeProperties properties;
|
||||
private final Clock clock;
|
||||
|
||||
public OrdsProbeService(
|
||||
BearerTokenService tokenService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
AuditService auditService,
|
||||
ProbeErrorClassifier errorClassifier,
|
||||
RestTemplate ordsRestTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
BackofficeProperties properties,
|
||||
Clock clock
|
||||
) {
|
||||
this.tokenService = tokenService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.auditService = auditService;
|
||||
this.errorClassifier = errorClassifier;
|
||||
this.ordsRestTemplate = ordsRestTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public ProbeResult runProbe(ProbeCommand command) {
|
||||
BearerTokenRecord token = tokenService.findById(command.keyId());
|
||||
if (token == null) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
|
||||
}
|
||||
if (!token.active(OffsetDateTime.now(clock))) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_INACTIVE, "TOKEN_INACTIVE", "만료되었거나 회수된 토큰입니다."));
|
||||
}
|
||||
if (!tokenService.matches(token, command.bearerToken())) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.INVALID_TOKEN, "INVALID_TOKEN", "입력한 Bearer Token이 선택한 key와 일치하지 않습니다."));
|
||||
}
|
||||
|
||||
ProtectedObject object;
|
||||
try {
|
||||
object = protectedObjectService.assertEnabled(command.objectId());
|
||||
} catch (AppException e) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.OBJECT_DISABLED, "OBJECT_DISABLED", e.getMessage()));
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = buildUri(object.ordsPath(), command.limit());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(command.bearerToken());
|
||||
ResponseEntity<String> response = ordsRestTemplate.exchange(
|
||||
uri, HttpMethod.POST, new HttpEntity<>(headers), String.class);
|
||||
ProbeResult result = parseSuccess(response.getBody(), object.objectId());
|
||||
return auditAndReturn(command, result);
|
||||
} catch (HttpStatusCodeException e) {
|
||||
ProbeStatus status = errorClassifier.classify(e.getStatusCode(), e.getResponseBodyAsString());
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
status, status.name(), trimMessage(e.getResponseBodyAsString())));
|
||||
} catch (ResourceAccessException e) {
|
||||
ProbeStatus status = errorClassifier.isTimeout(e) ? ProbeStatus.ORDS_TIMEOUT : ProbeStatus.UNKNOWN_ERROR;
|
||||
return auditAndReturn(command, ProbeResult.blocked(status, status.name(), e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.INVALID_ORDS_RESPONSE, "INVALID_ORDS_RESPONSE", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private URI buildUri(String ordsPath, int limit) {
|
||||
String baseUrl = properties.ords().baseUrl();
|
||||
String path = ordsPath.startsWith("/") ? ordsPath.substring(1) : ordsPath;
|
||||
return UriComponentsBuilder.fromUriString(baseUrl)
|
||||
.path("/")
|
||||
.path(path)
|
||||
.queryParam("limit", limit)
|
||||
.build()
|
||||
.toUri();
|
||||
}
|
||||
|
||||
private ProbeResult parseSuccess(String body, long objectId) throws Exception {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
JsonNode rowsNode = root.has("rows") ? root.get("rows") : root;
|
||||
rowsNode = root.has("items") ? root.get("items") : rowsNode;
|
||||
if (!rowsNode.isArray()) {
|
||||
throw new AppException("ORDS 응답에 rows 배열이 없습니다.");
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
Set<String> columns = new LinkedHashSet<>();
|
||||
for (JsonNode rowNode : rowsNode) {
|
||||
Map<String, Object> row = objectMapper.convertValue(rowNode, MAP_TYPE);
|
||||
rows.add(row);
|
||||
columns.addAll(row.keySet());
|
||||
}
|
||||
|
||||
ProbeStatus status = rows.isEmpty() ? ProbeStatus.VPD_DENY_EMPTY_RESULT : ProbeStatus.SUCCESS;
|
||||
return new ProbeResult(
|
||||
status,
|
||||
List.copyOf(columns),
|
||||
rows,
|
||||
rows.size(),
|
||||
findMaskedColumns(objectId, rows),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> findMaskedColumns(long objectId, List<Map<String, Object>> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> sensitiveColumns = protectedObjectService.findColumns(objectId).stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(column -> column.columnName().toLowerCase(Locale.ROOT))
|
||||
.toList();
|
||||
if (sensitiveColumns.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> masked = new ArrayList<>();
|
||||
for (String column : sensitiveColumns) {
|
||||
boolean present = false;
|
||||
boolean allNull = true;
|
||||
for (Map<String, Object> row : rows) {
|
||||
for (Map.Entry<String, Object> entry : row.entrySet()) {
|
||||
if (entry.getKey().equalsIgnoreCase(column)) {
|
||||
present = true;
|
||||
allNull = allNull && entry.getValue() == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (present && allNull) {
|
||||
masked.add(column);
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
|
||||
private ProbeResult auditAndReturn(ProbeCommand command, ProbeResult result) {
|
||||
auditService.record(new AuditEvent(
|
||||
"ORDS_PROBE",
|
||||
command.keyId(),
|
||||
command.objectId(),
|
||||
result.status().name(),
|
||||
result.rowCount(),
|
||||
result.errorCode(),
|
||||
result.errorMessage()
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
private String trimMessage(String body) {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
return body.length() <= 500 ? body : body.substring(0, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class PermissionService {
|
||||
|
||||
private static final Set<String> RULE_TYPES = Set.of("ALL", "MY_DEPT", "SELF", "REGION");
|
||||
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public PermissionService(
|
||||
PermissionMapper permissionMapper,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
AuditService auditService
|
||||
) {
|
||||
this.permissionMapper = permissionMapper;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
public List<AppRole> findRoles() {
|
||||
return permissionMapper.findRoles();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PermissionSet savePermissionSet(PermissionSetCommand command) {
|
||||
if (!"SELECT".equalsIgnoreCase(command.action())) {
|
||||
throw new AppException("초기 구현에서는 SELECT 권한만 저장할 수 있습니다.");
|
||||
}
|
||||
AppRole role = permissionMapper.findRole(command.roleId());
|
||||
if (role == null) {
|
||||
throw new AppException("역할을 찾을 수 없습니다.");
|
||||
}
|
||||
protectedObjectService.assertEnabled(command.objectId());
|
||||
validateRules(command.rules());
|
||||
validateVisibleColumns(command.objectId(), command.visibleColumns());
|
||||
|
||||
Long existingId = permissionMapper.findPermissionId(command.roleId(), command.objectId());
|
||||
long permissionId = existingId == null ? permissionMapper.nextPermissionId() : existingId;
|
||||
if (existingId == null) {
|
||||
permissionMapper.insertPermission(permissionId, command.roleId(), command.objectId(), "SELECT");
|
||||
} else {
|
||||
permissionMapper.updatePermissionAction(permissionId, "SELECT");
|
||||
}
|
||||
|
||||
permissionMapper.deleteRules(permissionId);
|
||||
for (RuleCommand rule : command.rules()) {
|
||||
permissionMapper.insertRule(new PermissionRule(
|
||||
permissionMapper.nextRuleId(),
|
||||
permissionId,
|
||||
normalize(rule.ruleType()),
|
||||
clean(rule.ruleValue())
|
||||
));
|
||||
}
|
||||
|
||||
permissionMapper.deleteVisibleColumns(permissionId);
|
||||
if (command.visibleColumns() != null) {
|
||||
for (String columnName : command.visibleColumns()) {
|
||||
permissionMapper.insertVisibleColumn(permissionId, columnName.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
auditService.record(new AuditEvent(
|
||||
"PERMISSION_SAVED", null, command.objectId(), "SUCCESS", null, null,
|
||||
"roleId=" + command.roleId()
|
||||
));
|
||||
return new PermissionSet(permissionId, command.roleId(), command.objectId(), "SELECT", List.of(), List.of());
|
||||
}
|
||||
|
||||
private void validateRules(List<RuleCommand> rules) {
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
throw new AppException("행 규칙은 하나 이상 필요합니다.");
|
||||
}
|
||||
Set<String> seen = new HashSet<>();
|
||||
boolean hasAll = false;
|
||||
for (RuleCommand rule : rules) {
|
||||
String type = normalize(rule.ruleType());
|
||||
if (!RULE_TYPES.contains(type)) {
|
||||
throw new AppException("허용되지 않은 행 규칙입니다: " + type);
|
||||
}
|
||||
if (!seen.add(type + ":" + clean(rule.ruleValue()))) {
|
||||
throw new AppException("중복된 행 규칙이 있습니다.");
|
||||
}
|
||||
hasAll = hasAll || "ALL".equals(type);
|
||||
if (!"ALL".equals(type) && clean(rule.ruleValue()).isBlank()) {
|
||||
throw new AppException(type + " 규칙에는 값이 필요합니다.");
|
||||
}
|
||||
}
|
||||
if (hasAll && rules.size() > 1) {
|
||||
throw new AppException("ALL 규칙은 다른 규칙과 함께 저장할 수 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVisibleColumns(long objectId, List<String> visibleColumns) {
|
||||
if (visibleColumns == null || visibleColumns.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> allowed = new HashSet<>();
|
||||
for (ProtectedColumn column : protectedObjectService.findColumns(objectId)) {
|
||||
allowed.add(column.columnName().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
for (String columnName : visibleColumns) {
|
||||
if (!allowed.contains(columnName.trim().toUpperCase(Locale.ROOT))) {
|
||||
throw new AppException("등록되지 않은 컬럼입니다: " + columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return clean(value).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
||||
import java.net.SocketTimeoutException;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
|
||||
@Component
|
||||
public class ProbeErrorClassifier {
|
||||
|
||||
public ProbeStatus classify(HttpStatusCode status, String body) {
|
||||
String text = body == null ? "" : body;
|
||||
if (text.contains("ORA-20002")) {
|
||||
return ProbeStatus.INVALID_TOKEN;
|
||||
}
|
||||
if (text.contains("ORA-00942") || text.contains("ORA-01031")) {
|
||||
return ProbeStatus.OBJECT_NOT_ACCESSIBLE;
|
||||
}
|
||||
if (status != null && (status.value() == 401 || status.value() == 403)) {
|
||||
return ProbeStatus.INVALID_TOKEN;
|
||||
}
|
||||
return ProbeStatus.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
public boolean isTimeout(ResourceAccessException exception) {
|
||||
Throwable cause = exception;
|
||||
while (cause != null) {
|
||||
if (cause instanceof SocketTimeoutException) {
|
||||
return true;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.ProtectedObjectMapper;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ProtectedObjectService {
|
||||
|
||||
private final ProtectedObjectMapper mapper;
|
||||
|
||||
public ProtectedObjectService(ProtectedObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public List<ProtectedObject> findEnabled() {
|
||||
return mapper.findEnabled();
|
||||
}
|
||||
|
||||
public ProtectedObject assertEnabled(long objectId) {
|
||||
ProtectedObject object = mapper.findById(objectId);
|
||||
if (object == null || !object.enabled()) {
|
||||
throw new AppException("활성 보호 객체를 찾을 수 없습니다.");
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
public List<ProtectedColumn> findColumns(long objectId) {
|
||||
return mapper.findColumns(objectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TokenGenerator {
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public String generate() {
|
||||
byte[] bytes = new byte[32];
|
||||
secureRandom.nextBytes(bytes);
|
||||
return "vpd_live_" + Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
|
||||
public String prefix(String token) {
|
||||
if (token == null || token.length() <= 16) {
|
||||
return token;
|
||||
}
|
||||
return token.substring(0, 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TokenHasher {
|
||||
|
||||
public String sha256(String token) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(digest.digest(token.getBytes(StandardCharsets.UTF_8))).toUpperCase();
|
||||
} catch (Exception e) {
|
||||
throw new AppException("Failed to hash bearer token");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user