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( public record AuthInfo(
Long authId, Long authId,
String dataId, String dataId,
Long merchId,
Long hierarchyId, Long hierarchyId,
String displayName, String displayName,
Boolean isEnabled, Boolean isEnabled,
Boolean needActivation, Boolean needActivation,
List<Permission> permissions, List<Permission> permissions,
SessionSettings sessionSettings,
JsonNode sessionData, JsonNode sessionData,
JsonNode privateData, JsonNode privateData,
SecretData secretData 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 username: AUTH
password: .,z;pws-2shds password: .,z;pws-2shds
driver-class-name: oracle.jdbc.OracleDriver 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