使用 SPA 客户端和带有 Spring Security 的 Web API 启动 OAuth2 授权代码流程

问题描述 投票:0回答:1

我有一个 java web api,它配置了 Spring Security 和 AWS Cognito OAuth2 提供程序。我有单独的 Web 应用程序 (ReactJs),它调用 api,因此 ui 需要登录受保护的端点。

单击“登录”按钮时,单独托管的网络应用程序将运行以下代码:

window.location.assign("http://<api-server:port>/oauth2/authorization/cognito)

最终重定向到

https://<provider-url>/oauth2/authorize?response_type=code&client_id=xxx&scope=openid&state=???&redirect_uri=http://<api-server:port>/login/oauth2/code/cognito&nonce=???
来启动流程。这似乎可行,但是这是正确的方法吗?另外,我如何设置状态参数(我需要它来存储发起调用的网页的 url,以便我可以让用户在登录后返回到发起页面。目前这是硬编码在 SimpleUrlAuthenticationSuccessHandler.onAuthenticationSuccess 中)

另一种方法似乎是直接调用授权端点

https://<provider-url>/oauth2/authorize?response_type=code&client_id=xxx&scope=openid&state=???&redirect_uri=http://<api-server:port>/login/oauth2/code/cognito&nonce=???

在这种情况下,在浏览器上维护 client_id 是否安全?如果可以,我应该为 nonce 和 state 提供什么。使用之前的方法,它会自动填充这些值,其中状态是一些无法破译的值。

reactjs spring spring-security oauth-2.0
1个回答
0
投票

在将用户浏览器发送到您的授权服务器之前,您必须确保他在 Spring OAuth2 客户端上具有

oauth2Login
的会话(为您要设置的参数创建值)。

授权服务器的配置和 OAuth2 客户端注册已经在 Spring 应用程序中。

为了避免配置重复(和差异),并确保前端有一个打开的会话,我通常按照我编写的本教程中的方式进行处理:Spring OAuth2 客户端公开可以启动授权代码流的 URI,前端调用该端点开始登录。类似这样的东西: @RestController @Observed(name = "GatewayController") public class GatewayController { private final SpringAddonsOidcClientProperties addonsClientProperties; private final List<LoginOptionDto> loginOptions; public GatewayController(OAuth2ClientProperties clientProps, SpringAddonsOidcProperties addonsProperties) { this.addonsClientProperties = addonsProperties.getClient(); this.loginOptions = clientProps .getRegistration() .entrySet() .stream() .filter(e -> "authorization_code".equals(e.getValue().getAuthorizationGrantType())) .map(e -> new LoginOptionDto(e.getValue().getProvider(), "%s/oauth2/authorization/%s".formatted(addonsClientProperties.getClientUri(), e.getKey()))) .toList(); } @GetMapping(path = "/login-options", produces = "application/json") public Flux<LoginOptionDto> getLoginOptions(Authentication auth) throws URISyntaxException { final boolean isAuthenticated = auth instanceof OAuth2AuthenticationToken; return Flux.fromStream(isAuthenticated ? Stream.empty() : this.loginOptions.stream()); } static record LoginOptionDto(@NotEmpty String label, @NotEmpty String loginUri) {} }

我还使用自定义 
302

将授权代码流中第一个重定向的响应状态从

2xx
更改为
(Server)RedirectStrategy
范围内的状态(界面取决于应用程序是 servlet 还是响应式应用程序) :
http.oauth2Login(oauth2 -> {
    oauth2.authorizationRedirectStrategy(preAuthorizationCodeRedirectStrategy);
    ...
});

在 servlet 中使用类似的东西(可能将 
204 - No content

作为构造函数参数,并为前端提供在请求上使用自定义标头强制状态的能力):

@RequiredArgsConstructor
public class SpringAddonsOauth2RedirectStrategy implements RedirectStrategy {
    public static final String RESPONSE_STATUS_HEADER = "X-RESPONSE-STATUS";

    private final HttpStatus defaultStatus;

    @Override
    public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String location) throws IOException {
        final var requestedStatus = request.getIntHeader(RESPONSE_STATUS_HEADER);
        response.setStatus(requestedStatus > -1 ? requestedStatus : defaultStatus.value());

        response.setHeader(HttpHeaders.LOCATION, location);
    }
}

在反应式应用程序中就像这样:

@RequiredArgsConstructor public class SpringAddonsOauth2ServerRedirectStrategy implements ServerRedirectStrategy { public static final String RESPONSE_STATUS_HEADER = "X-RESPONSE-STATUS"; private final HttpStatus defaultStatus; @Override public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) { return Mono.fromRunnable(() -> { ServerHttpResponse response = exchange.getResponse(); final var status = Optional .ofNullable(exchange.getRequest().getHeaders().get(RESPONSE_STATUS_HEADER)) .map(List::stream) .orElse(Stream.empty()) .filter(StringUtils::hasLength) .findAny() .map(statusStr -> { try { final var statusCode = Integer.parseInt(statusStr); return HttpStatus.valueOf(statusCode); } catch (NumberFormatException e) { return HttpStatus.valueOf(statusStr.toUpperCase()); } }) .orElse(defaultStatus); response.setStatusCode(status); response.getHeaders().setLocation(location); }); } }

状态处于 
2xx

范围内时,浏览器不会处理重定向,因此前端可以观察响应并通过更改原点来跟踪

Location
(像您已经做的那样设置
window.location
,但遵循后端提供的 URL,已设置状态、随机数等)。
    

© www.soinside.com 2019 - 2024. All rights reserved.