我有一个 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 提供什么。使用之前的方法,它会自动填充这些值,其中状态是一些无法破译的值。
在将用户浏览器发送到您的授权服务器之前,您必须确保他在 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,已设置状态、随机数等)。