Spring 启动更改默认身份验证系统

Spring boot change default authentication system

默认spring 引导使用HttpServletRequest 从客户端默认接受值到"/login" 路由。

如果我想创建一个自定义身份验证系统,选项如下:

  1. 登录端点,如:“/api/v1/auth/sign”将接受电子邮件和密码
  2. 与其创建 CustomAuthFilter(如 YT 中的许多视频所示),不如在 auth 控制器中创建一个方法来处理发回 jwt 令牌。

现在我已经知道要更改默认登录路径我需要:

.formLogin().loginProcessingUrl("/api/v1/login")

但是下一部分呢?

我是否需要创建像 SignInRequestSignInResponse 这样的对象?

如果是这样,客户端应用程序是否需要根据 SignInRequestSignInResponse 映射数据?

这是我的 Signup 服务:

@Override
public User signup(User user) {
    String encodedPassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
    return authRepository.save(user);
}

我想为登录创建一个类似的服务,例如:

@Override
public User signin(String email, String password) {
   // somehow do login and return the user with access and refresh tokens?
}

即使我创建一个 SigninRequest 对象,客户端应用程序也会始终发送电子邮件和密码,对吗?

由于我没有使用过复杂的后端,所以我对如何解决这个问题的想法非常有限。

任何见解或资源都会有所帮助,谢谢。

我目前的attemptAuthentication方法:

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        StringBuffer sb = new StringBuffer();
        BufferedReader reader = null;
        String content = "";
        String email = "";
        String password = "";
        try {
            reader = request.getReader();
            char[] buffer = new char[1024];
            int read;
            while ((read = reader.read(buffer, 0, buffer.length)) != -1) {
                sb.append(buffer, 0, read);
            }
            content = sb.toString();
            Map<String, String> map = new ObjectMapper().readValue(content, Map.class);
            email = map.get("email");
            password = map.get("password");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ex) {
                    try {
                        throw ex;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
        return authenticationManager.authenticate(authenticationToken);
    }

Spring 支持许多复杂的自定义身份验证方法。 spring 安全文档中提供了完整的概述:

Spring Security Docs - Authentication

根据您的问题,我得出的结论是您想坚持某种 user/password 身份验证。这在此处的文档中进行了描述:

Spring Security Docs - User/Password Authentication

你基本上有三个选择:

  1. 表单登录:在这里,一个动态生成的 HTML 页面呈现给用户,他可以在其中输入用户(在您的例子中是电子邮件地址)和密码。
  2. 基本身份验证:这里不需要单独的 HTML 页面。相反,浏览器会直接显示一个弹出对话框,用户可以在其中输入他的凭据。
  3. 摘要式身份验证:相当不常见,spring.org 不推荐。

同样,根据你的问题,我得出的结论是你想坚持使用表单登录。 Spring 为这种替代方案提供了许多选项,这些选项在上述文档中有广泛介绍。

另一个很好的起点是以下流行教程:

Baeldung Tutorial about Spring Security Form Login

除了单纯的实施方面,这里当然还有许多涉及身份验证方法问题的安全考虑因素:User/password身份验证被认为是一种相当不安全的身份验证形式,因为它需要交换用户和服务器之间的秘密。更安全的形式是例如使用一次性密码或证书(例如由浏览器处理的 SSL 客户端证书)。作为进一步阅读的起点,我可以建议:

Rising Stack - Web Authentication Methods Explained

Port Swigger - Authentication vulnerabilities

为了更清楚地说明,我在此处添加了一些示例代码,说明如何使用 React 客户端进行表单登录。此示例基于 Spring 框架中的反应式 WebTestClient。

表单登录涉及以下步骤:

  1. 客户端发送请求(GET 或 POST),其中包含所需的 URI(可以是静态或动态 HTML 页面或 REST 资源)。
  2. 当服务器识别出所请求的 URI 需要身份验证时,他会使用指向登录页面的 HTTP 重定向状态代码 (302) 进行响应。
  3. 现在客户端通过向登录页面发送 GET 请求来启动登录序列。虽然步骤 1 和 2 是可选的并且可以省略,但此步骤至关重要,因为它会在服务器上启动 session。
  4. 当收到对登录页面的请求时,服务器会创建一个新的 HTTP session、一个 CSRF 令牌(请参阅 Spring 文档),并生成动态登录页面。
  5. 客户端回复 POST 请求,其中包括 user/password 和 body 中的 CSRF 令牌以及请求 header 中的 session id (例如作为 cookie)。
  6. 当一切正确并且没有省略第 1 步和第 2 步时,服务器响应重定向到第 1 步中最初请求的资源(如果省略第 1 步和第 2 步,则重定向到默认登录页面)。
  7. 现在客户端可以再次请求想要的资源了。对于以下所有请求,它必须在请求 header.
  8. 中包含 session id

这里是 Java 中的测试客户端实现,它在客户端执行此序列:

public class WebTestClientUtil {

    public static class ResponseHolder {
        public String baseUrl;
        public HttpStatus status;
        public String body;
        public String sessionId;
        public String csrfToken;
        public String location;

        public ResponseHolder(boolean sslEnabled, int port) {
            this.baseUrl = getBaseUrl(sslEnabled, port);
        }
    }

    public enum SessionIdResolutionMethod {
        HEADER,
        COOKIE,
        URL
    }

    /**
     * Determine the server base url
     *
     * @param port of the server to connect to
     * @return the server url
     */
    static public String getBaseUrl(boolean sslEnabled, int port) {
        return "http" + (sslEnabled ? "s" : "") + "://localhost:" + port + "/";
    }

    /**
     * Build a client for testing
     *
     * @param ref the response holder to be used for further communication
     * @return the client
     */
    public static WebTestClient buildClient(ResponseHolder ref) {
        return WebTestClient
                .bindToServer()
                .baseUrl(ref.baseUrl)
                .responseTimeout(Duration.ofMinutes(10))  // uncomment for debugging
                .build();
    }

    /**
     * Perform a form login and return a response spec to formulate expectations about the result
     *
     * @param client   the client to be used for connecting to the server
     * @param uri      for the request
     * @param username for authentication
     * @param password for authentication
     * @param ref      container for the server response
     * @return the response spec
     */
    public static WebTestClient.ResponseSpec performLoginSequence(
            WebTestClient client,
            String uri,
            String username,
            String password,
            ResponseHolder ref) {
        return performLoginSequence(client, uri, username, password, ref, SessionIdResolutionMethod.HEADER);
    }

    /**
     * Perform a form login and return a response spec to formulate expectations about the result
     *
     * @param client                    the client to be used for connecting to the server
     * @param uri                       for the request
     * @param username                  for authentication
     * @param password                  for authentication
     * @param ref                       container for the server response
     * @param sessionIdResolutionMethod true if cookies shall be used for session id resolution
     * @return the response spec
     */
    public static WebTestClient.ResponseSpec performLoginSequence(
            WebTestClient client,
            String uri,
            String username,
            String password,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        System.out.println();
        System.out.println("----------------------------------------------------------------------");
        System.out.println("New login sequence initiated for user " + username + " with password " + password + "...");
        System.out.println("----------------------------------------------------------------------");

        // Send api request
        evaluateResponse(client.get().uri(uri).exchange(), ref, sessionIdResolutionMethod)
                .expectStatus().is3xxRedirection()
                .expectHeader().location(ref.baseUrl + "login");

        // Send login request
        evaluateResponse(getRequestSpec(client, "login", ref, sessionIdResolutionMethod)
                .accept(MediaType.TEXT_HTML)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), ref, sessionIdResolutionMethod)
                .expectStatus().isOk();
        assertThat(ref.body).contains("name=\"username\"").contains("name=\"password\"").contains("name=\"_csrf\"");
        assertThat(ref.csrfToken).isNotNull();

        // Send login details
        WebTestClient.ResponseSpec response =
                evaluateResponse(postRequestSpec(client, "login", ref, sessionIdResolutionMethod)
                        .body(BodyInserters
                                .fromFormData("_csrf", ref.csrfToken)
                                .with("username", username)
                                .with("password", password))
                        .exchange(), ref, sessionIdResolutionMethod);

        // In case of errors abort login process
        System.out.println("Login sequence completed...");
        System.out.println("----------------------------------------------------------------------");
        if (ref.status != HttpStatus.FOUND) return response;
        if (!ref.location.equals(ref.baseUrl + uri)) return response;

        // After successful login continue with redirect
        System.out.println("Continuing with original request " + uri + "...");
        return evaluateRedirect(client, ref, sessionIdResolutionMethod);
    }

    public static WebTestClient.ResponseSpec evaluateResponse(WebTestClient.ResponseSpec response, ResponseHolder r) {
        return evaluateResponse(response, r, SessionIdResolutionMethod.HEADER);
    }

    public static WebTestClient.ResponseSpec evaluateResponse(
            WebTestClient.ResponseSpec response,
            ResponseHolder r,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        ExchangeResult result = response
                .expectBody(String.class).consumeWith(v -> r.body = v.getResponseBody())
                .returnResult();
        r.status = result.getStatus();
        System.out.println();
        System.out.println("Response for request " + result.getUrl());
        System.out.println("  HTTP status = " + r.status);

        String sessionId = null;
        switch (sessionIdResolutionMethod) {
            case HEADER, URL -> sessionId = result.getResponseHeaders().getFirst(ResponseHeaderFilter.SESSION_ID_HEADER_NAME);
            case COOKIE -> {
                ResponseCookie sessionIdCookie = result.getResponseCookies().getFirst("SESSION");
                if (sessionIdCookie != null) sessionId = sessionIdCookie.getValue();
            }
        }
        if (sessionId == null) {
            System.out.println("  Session id not set in " + sessionIdResolutionMethod +
                    " - continuing with old (" + r.sessionId + ")");
        } else {
            r.sessionId = sessionId;
            System.out.println("  Session id --> " + r.sessionId);
        }

        if (r.body == null) r.body = "";
        String csrfToken = getCsrfToken(r.body);
        if (csrfToken != null) r.csrfToken = csrfToken;
        csrfToken = result.getResponseHeaders().getFirst(ResponseHeaderFilter.CSRF_HEADER_NAME);
        if (csrfToken != null) {
            csrfToken = result.getResponseHeaders().getFirst(csrfToken);
            if (csrfToken != null) r.csrfToken = csrfToken;
        }
        System.out.println("  CSRF token = " + csrfToken + " --> " + r.csrfToken);

        System.out.println("  Response Headers:");
        for (Map.Entry<String, List<String>> header : result.getResponseHeaders().entrySet()) {
            System.out.println("    " +
                    header.getKey() + ": " +
                    String.join("; ", header.getValue()));
        }
        r.location = result.getResponseHeaders().getFirst("Location");

        System.out.println("  Body:\n" + r.body);
        return response;
    }

    public static WebTestClient.RequestHeadersSpec<?> getRequestSpec(
            WebTestClient client,
            String uri,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        switch (sessionIdResolutionMethod) {
            case HEADER -> {
                WebTestClient.RequestHeadersSpec<?> request = client.get().uri(uri);
                return request.header(ResponseHeaderFilter.SESSION_ID_HEADER_NAME, ref.sessionId);
            }
            case COOKIE -> {
                WebTestClient.RequestHeadersSpec<?> request = client.get().uri(uri);
                return request.cookie("SESSION", ref.sessionId);
            }
            case URL -> {
                return client.get().uri(uri +
                        (uri.contains("?") ? "&" : "?") +
                        "xAuthToken=" +
                        new String(Base64.getEncoder().encode(ref.sessionId.getBytes())));
            }
        }
        throw new IllegalArgumentException("Invalid session id resolution method: " + sessionIdResolutionMethod);
    }

    public static WebTestClient.RequestBodySpec postRequestSpec(
            WebTestClient client,
            String uri,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        switch (sessionIdResolutionMethod) {
            case HEADER -> {
                WebTestClient.RequestBodySpec request = client.post().uri(uri);
                return request.header(ResponseHeaderFilter.SESSION_ID_HEADER_NAME, ref.sessionId);
            }
            case COOKIE -> {
                WebTestClient.RequestBodySpec request = client.post().uri(uri);
                return request.cookie("SESSION", ref.sessionId);
            }
            case URL -> {
                return client.post().uri(uri +
                        (uri.contains("?") ? "&" : "?") +
                        "xAuthToken=" +
                        new String(Base64.getEncoder().encode(ref.sessionId.getBytes())));
            }
        }
        throw new IllegalArgumentException("Invalid session id resolution method: " + sessionIdResolutionMethod);
    }

    public static String getCsrfToken(String body) {
        int pos = body.indexOf("name=\"_csrf\"");
        if (pos < 0) return null;
        int start = body.indexOf("value=\"", pos) + 7;
        if (start < 0) return null;
        int end = body.indexOf("\"", start);
        if (end < 0) return null;
        return body.substring(start, end);
    }

    @SuppressWarnings("UnusedReturnValue")
    public static WebTestClient.ResponseSpec evaluateRedirect(WebTestClient client, ResponseHolder r) {
        return evaluateRedirect(client, r, SessionIdResolutionMethod.HEADER);
    }

    public static WebTestClient.ResponseSpec evaluateRedirect(WebTestClient client, ResponseHolder r, SessionIdResolutionMethod sessionIdResolutionMethod) {
        return evaluateResponse(getRequestSpec(client, r.location, r, sessionIdResolutionMethod)
                .accept(MediaType.APPLICATION_JSON)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), r, sessionIdResolutionMethod);
    }

    public static void performLogout(WebTestClient client, ResponseHolder r) {
        performLogout(client, r, SessionIdResolutionMethod.HEADER);
    }

    public static void performLogout(WebTestClient client, ResponseHolder r, SessionIdResolutionMethod sessionIdResolutionMethod) {
        evaluateResponse(postRequestSpec(client, r.baseUrl + "logout", r, sessionIdResolutionMethod)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), r, sessionIdResolutionMethod)
                .expectStatus().is3xxRedirection();
        assertThat(r.location).isEqualTo(r.baseUrl + "login?logout");
    }
}