add auth controller

This commit is contained in:
root
2025-11-28 17:29:37 +03:00
parent 47f64dc4b8
commit f32de323bf
12 changed files with 662 additions and 1 deletions

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View File

@@ -0,0 +1,62 @@
package ru.copperside.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@Data
@Component
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
private String endpoint = "/auth";
private Headers headers = new Headers();
private Response response = new Response();
private Logging logging = new Logging();
private HeaderForwarding headerForwarding = new HeaderForwarding();
private Set<String> returnSessionPaths = new HashSet<>();
public boolean shouldReturnSessionForPath(String sourcePath) {
return returnSessionPaths.contains(sourcePath);
}
@Data
public static class Headers {
private String login;
private String signature;
private String originalPath;
}
@Data
public static class Response {
private String sessionFieldName = "SessionId";
}
@Data
public static class Logging {
private boolean logRequestBody = true;
private boolean logResponseBody = true;
private boolean logHeaders = true;
}
@Data
public static class HeaderForwarding {
private boolean enabled = true;
private Set<String> exclude = new HashSet<>();
public boolean isExcluded(String headerName) {
if (headerName == null) return true;
String lower = headerName.toLowerCase(Locale.ROOT);
return exclude.stream()
.map(s -> s.toLowerCase(Locale.ROOT))
.anyMatch(lower::equals);
}
}
}

View File

@@ -0,0 +1,112 @@
package ru.copperside.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.copperside.config.AuthProperties;
import ru.copperside.logging.AuthLogger;
import ru.copperside.service.AuthenticationService;
import java.util.Map;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping
public class AuthController {
private final AuthenticationService authenticationService;
private final ObjectMapper objectMapper;
private final AuthProperties props;
private final AuthLogger authLogger;
@PostMapping(
path = "${auth.endpoint}",
consumes = "application/json",
produces = "application/json"
)
public ResponseEntity<JsonNode> authenticate(
@RequestBody(required = false) JsonNode body,
HttpServletRequest request
) {
// Собираем ВСЕ заголовки
Map<String, String> headers = authLogger.extractHeaders(request);
// Пробуем сохранить бизнес-URI (X-Original-Path или что ты настроишь)
String originalPathHeaderName = props.getHeaders().getOriginalPath();
if (originalPathHeaderName != null) {
String originalPath = headers.get(originalPathHeaderName);
if (originalPath != null && !originalPath.isBlank()) {
request.setAttribute("ORIGINAL_PATH", originalPath);
}
}
// Логируем запрос (по конфигу)
authLogger.logRequest(request, body, headers);
// Валидация логина/подписи, прав и логика сессии — ТОЛЬКО в сервисе
authenticationService.authenticate(headers, body, request);
// Формируем тело ответа: по контракту возвращаем исходный body
JsonNode responseBody = body;
// При необходимости добавляем SessionId
if (Boolean.TRUE.equals(request.getAttribute("RETURN_SESSION"))) {
String sessionId = (String) request.getAttribute("SESSION_ID");
responseBody = addSessionToBody(body, sessionId);
}
// Пробрасываем заголовки назад (за исключением служебных)
HttpHeaders responseHeaders = buildForwardHeaders(headers);
// Логируем ответ (по конфигу)
authLogger.logResponse(request, responseBody);
return ResponseEntity.ok()
.headers(responseHeaders)
.body(responseBody);
}
private HttpHeaders buildForwardHeaders(Map<String, String> headers) {
HttpHeaders resp = new HttpHeaders();
var cfg = props.getHeaderForwarding();
if (!cfg.isEnabled()) {
return resp;
}
headers.forEach((name, value) -> {
if (!cfg.isExcluded(name)) {
resp.add(name, value);
}
});
return resp;
}
private JsonNode addSessionToBody(JsonNode body, String sessionId) {
String field = props.getResponse().getSessionFieldName();
if (body == null || body.isNull()) {
ObjectNode node = objectMapper.createObjectNode();
node.put(field, sessionId);
return node;
}
if (body.isObject()) {
((ObjectNode) body).put(field, sessionId);
return body;
}
ObjectNode wrapper = objectMapper.createObjectNode();
wrapper.set("data", body);
wrapper.put(field, sessionId);
return wrapper;
}
}

View File

@@ -0,0 +1,97 @@
package ru.copperside.exception;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.copperside.exception.dto.ErrorResponseDto;
import ru.copperside.logging.AuthLogger;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Optional;
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private static final String EXCEPTION_TYPE = "Error";
private final AuthLogger authLogger;
@ExceptionHandler(InvalidUserNameOrSignatureException.class)
public ResponseEntity<ErrorResponseDto> handleInvalidUserNameOrSignature(
InvalidUserNameOrSignatureException ex,
HttpServletRequest request
) {
String sourcePath = resolveSourcePath(request);
String sessionId = resolveSessionId(request);
ErrorResponseDto body = ErrorResponseDto.builder()
.ExceptionType(EXCEPTION_TYPE)
.Code("InvalidUserNameOrSignature")
.Message("InvalidUserNameOrSignature")
.AdapterName("")
.SourceType("")
.SourcePath(sourcePath)
.SessionId(sessionId.isEmpty() ? null : sessionId)
.TimeStamp(OffsetDateTime.now())
.Properties(Map.of("Cause", "Данные не соответствуют подписи"))
.build();
int status = HttpStatus.UNAUTHORIZED.value();
// логируем в структурированный лог
authLogger.logError(request, body, status);
// при желании оставляем и "человеческий" лог
log.warn("InvalidUserNameOrSignature: {}", ex.getMessage());
return ResponseEntity.status(status).body(body);
}
@ExceptionHandler(InsufficientPermissionException.class)
public ResponseEntity<ErrorResponseDto> handleInsufficientPermission(
InsufficientPermissionException ex,
HttpServletRequest request
) {
String sourcePath = resolveSourcePath(request);
String sessionId = resolveSessionId(request);
ErrorResponseDto body = ErrorResponseDto.builder()
.ExceptionType(EXCEPTION_TYPE)
.Code("InsufficientPermission")
.Message("")
.AdapterName("")
.SourceType("")
.SourcePath(sourcePath)
.SessionId(sessionId.isEmpty() ? "" : sessionId)
.TimeStamp(OffsetDateTime.now())
.build();
int status = HttpStatus.FORBIDDEN.value();
authLogger.logError(request, body, status);
log.warn("InsufficientPermission: {}", ex.getMessage());
return ResponseEntity.status(status).body(body);
}
private String resolveSourcePath(HttpServletRequest request) {
Object attr = request.getAttribute("ORIGINAL_PATH");
if (attr instanceof String s && !s.isBlank()) {
return s;
}
return Optional.ofNullable(request.getRequestURI()).orElse("");
}
private String resolveSessionId(HttpServletRequest request) {
return Optional.ofNullable(request.getSession(false))
.map(s -> s.getId())
.orElse("");
}
}

View File

@@ -0,0 +1,10 @@
package ru.copperside.exception;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class InsufficientPermissionException extends RuntimeException {
public InsufficientPermissionException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package ru.copperside.exception;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class InvalidUserNameOrSignatureException extends RuntimeException {
public InvalidUserNameOrSignatureException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,24 @@
package ru.copperside.exception.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import java.time.OffsetDateTime;
import java.util.Map;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponseDto {
private String ExceptionType;
private String Code;
private String Message;
private String AdapterName;
private String SourceType;
private String SourcePath;
private String SessionId;
private OffsetDateTime TimeStamp;
private Map<String, Object> Properties;
}

View File

@@ -0,0 +1,92 @@
package ru.copperside.logging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.copperside.config.AuthProperties;
import ru.copperside.exception.dto.ErrorResponseDto;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthLogger {
private final AuthProperties props;
private final ObjectMapper mapper;
public void logRequest(HttpServletRequest request,
JsonNode body,
Map<String, String> headers) {
var cfg = props.getLogging();
if (!cfg.isLogHeaders() && !cfg.isLogRequestBody()) return;
try {
Map<String, Object> event = new LinkedHashMap<>();
event.put("event", "auth_request");
event.put("method", request.getMethod());
event.put("path", request.getRequestURI());
event.put("remoteAddr", request.getRemoteAddr());
if (cfg.isLogHeaders()) event.put("headers", headers);
if (cfg.isLogRequestBody()) event.put("body", body);
log.info(mapper.writeValueAsString(event));
} catch (Exception e) {
log.warn("Failed to log request", e);
}
}
public void logResponse(HttpServletRequest request, JsonNode body) {
var cfg = props.getLogging();
if (!cfg.isLogResponseBody()) return;
try {
Map<String, Object> event = new LinkedHashMap<>();
event.put("event", "auth_response");
event.put("path", request.getRequestURI());
event.put("status", 200);
event.put("body", body);
log.info(mapper.writeValueAsString(event));
} catch (Exception e) {
log.warn("Failed to log response", e);
}
}
/**
* Логирование ОШИБОЧНОГО ответа.
* Логируем всегда, независимо от флагов (ошибки терять нельзя).
*/
public void logError(HttpServletRequest request,
ErrorResponseDto errorBody,
int status) {
try {
Map<String, Object> event = new LinkedHashMap<>();
event.put("event", "auth_error");
event.put("path", request.getRequestURI());
event.put("status", status);
event.put("error", errorBody);
log.warn(mapper.writeValueAsString(event));
} catch (Exception e) {
log.warn("Failed to log error response", e);
}
}
public Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> names = request.getHeaderNames();
while (names != null && names.hasMoreElements()) {
String n = names.nextElement();
map.put(n, request.getHeader(n));
}
return map;
}
}

View File

@@ -13,12 +13,12 @@ import java.util.List;
public record AuthInfo(
Long authId,
String dataId,
Long merchId,
Long hierarchyId,
String displayName,
Boolean isEnabled,
Boolean needActivation,
List<Permission> permissions,
SessionSettings sessionSettings,
JsonNode sessionData,
JsonNode privateData,
SecretData secretData

View File

@@ -0,0 +1,15 @@
package ru.copperside.service;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
public interface AuthenticationService {
void authenticate(
Map<String, String> headers,
JsonNode body,
HttpServletRequest request
);
}

View File

@@ -0,0 +1,81 @@
package ru.copperside.service;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.copperside.service.AuthenticationService;
import ru.copperside.config.AuthProperties;
import ru.copperside.exception.InsufficientPermissionException;
import ru.copperside.exception.InvalidUserNameOrSignatureException;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
private final AuthProperties props;
@Override
public void authenticate(Map<String, String> headers,
JsonNode body,
HttpServletRequest request) {
// 1. Достаём логин и подпись из заголовков
String loginHeaderName = props.getHeaders().getLogin();
String signatureHeaderName = props.getHeaders().getSignature();
String login = headers.get(loginHeaderName);
String signature = headers.get(signatureHeaderName);
if (isBlank(login) || isBlank(signature)) {
// Отсутствие нужных заголовков — это та же ошибка InvalidUserNameOrSignature
throw new InvalidUserNameOrSignatureException("Missing login or signature header");
}
// 2. Проверка подписи (заглушка; здесь можешь использовать login/signature/body/headers)
boolean signatureOk = true; // TODO: реальная проверка HMAC
if (!signatureOk) {
throw new InvalidUserNameOrSignatureException("Invalid signature");
}
// 3. Проверка прав (заглушка; тут уже знаешь authId/permissions и т.д.)
boolean hasPerm = true; // TODO: реальная проверка прав
if (!hasPerm) {
throw new InsufficientPermissionException("Insufficient permissions");
}
// 4. Работа с SessionId при необходимости
String sourcePath = resolveSourcePath(request);
if (props.shouldReturnSessionForPath(sourcePath)) {
HttpSession session = request.getSession(true);
request.setAttribute("RETURN_SESSION", Boolean.TRUE);
request.setAttribute("SESSION_ID", session.getId());
}
}
private String resolveSourcePath(HttpServletRequest req) {
Object attr = req.getAttribute("ORIGINAL_PATH");
if (attr instanceof String s && !s.isBlank()) {
return s;
}
String headerName = props.getHeaders().getOriginalPath();
String header = headerName != null ? req.getHeader(headerName) : null;
if (header != null && !header.isBlank()) {
return header;
}
return Optional.ofNullable(req.getRequestURI()).orElse("");
}
private boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
}

View File

@@ -7,3 +7,37 @@ spring:
username: AUTH
password: .,z;pws-2shds
driver-class-name: oracle.jdbc.OracleDriver
auth:
endpoint: /auth
headers:
login: TCB-Header-Login
signature: TCB-Header-Sign
original-path: X-Original-Path
response:
session-field-name: SessionId
return-session-paths:
- /api/v1/order/state
logging:
log-request-body: true
log-response-body: true
log-headers: true
header-forwarding:
enabled: true
# Заголовки, которые НЕ нужно возвращать (регистр игнорируем)
exclude:
- content-length
- host
- connection
- transfer-encoding
- keep-alive
- proxy-authenticate
- proxy-authorization
- te
- trailer
- upgrade