add auth controller
This commit is contained in:
124
.idea/uiDesigner.xml
generated
Normal file
124
.idea/uiDesigner.xml
generated
Normal 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>
|
||||
62
src/main/java/ru/copperside/config/AuthProperties.java
Normal file
62
src/main/java/ru/copperside/config/AuthProperties.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/main/java/ru/copperside/controller/AuthController.java
Normal file
112
src/main/java/ru/copperside/controller/AuthController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.copperside.exception;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class InsufficientPermissionException extends RuntimeException {
|
||||
public InsufficientPermissionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.copperside.exception;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class InvalidUserNameOrSignatureException extends RuntimeException {
|
||||
public InvalidUserNameOrSignatureException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
92
src/main/java/ru/copperside/logging/AuthLogger.java
Normal file
92
src/main/java/ru/copperside/logging/AuthLogger.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user