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(
|
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
|
||||||
|
|||||||
@@ -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
|
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
|
||||||
Reference in New Issue
Block a user