Spring Boot Oauth 客户端和授权服务器 + React 实现

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

目前我开始实施 BFF(frotnend 的后端 - spring oauth 2 客户端),目的是为我的前端(react)提供服务,以便通过授权服务器进行身份验证。

我想弄清楚如何准确地使用 spring oauth 2 客户端来实现前端 - 授权工作流。

到目前为止,我在 spring boot 项目上有一个简单的 oauth2-client:

@Configuration
public class Security {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return   http.cors(cors -> cors.configurationSource(request -> {
                    var corsConfiguration = new CorsConfiguration();
                    corsConfiguration.addAllowedOrigin("http://127.0.0.1:3000");
                    corsConfiguration.setAllowCredentials(true);
                    corsConfiguration.addAllowedMethod("*");
                    corsConfiguration.addAllowedHeader("*");
                    return corsConfiguration;
                }))
                .csrf()
                .disable()
                .authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2Login( oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/securio"))
                .oauth2Client(Customizer.withDefaults())
                .build();

    }

}

我想有一个 get /userinfo 端点,它会在每次需要加载页面时检索用户(前端)的角色,以检查它是否具有必要的权限。

@Controller
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthenticationController {

    private final RestTemplate restTemplate;
    private final OAuth2AuthorizedClientService authorizedClientService;


     @GetMapping("/userinfo")
public ResponseEntity<UserInfo> getUserInfo() throws ParseException {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    var client = authorizedClientService.loadAuthorizedClient(
            ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(),
            authentication.getName());
    var accessToken = client.getAccessToken().getTokenValue();

    JWT jwt = JWTParser.parse(accessToken);

    List<String> authorities = jwt.getJWTClaimsSet().getStringListClaim("authorities");
    String userRole = null;
    for (String authority : authorities) {
        if (authority.startsWith("ROLE_")) {
            userRole = authority;
            break;
        }
    }
    if (userRole == null) {
        return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
    }

    String username = jwt.getJWTClaimsSet().getSubject();
    

    return new ResponseEntity<>(UserInfo.builder()
            .username(username)
            .role(userRole)
            .build(), HttpStatus.OK);

}

    @PostMapping("/logout")
    @ResponseStatus(HttpStatus.OK)
    public void logout(HttpServletRequest request, HttpServletResponse response) {

        HttpSession session = request.getSession(false);
        if (session != null) {

            ResponseEntity<Void> responseEntity = restTemplate.exchange(
                    "http://127.0.0.1:8082/auth/logout", HttpMethod.POST, null, Void.class);
            if (responseEntity.getStatusCode() != HttpStatus.NO_CONTENT) {
                throw new RuntimeException("Logout failed");
            }

            session.invalidate();

            Cookie cookie = new Cookie("JSESSIONID", "");
            cookie.setMaxAge(0);
            cookie.setPath("/");
            response.addCookie(cookie);
        } else {
            throw new RuntimeException("User already logged out");
        }

    }

}

这是 oauth2-client 的 application.yml:

server:
  port: 8081

logging:
  level:
    org.springframework:
      security: trace

spring:
  security:
    oauth2:
      client:
        registration:
          securio:
            client-id: securio
            client-secret: securio-secret
            authorization-grant-type: authorization_code
            redirect-uri: http://127.0.0.1:8081/login/oauth2/code/securio
            scope: openid
            provider: securio
        provider:
          securio:
            issuer-uri: http://localhost:8082

这就是我获取用户信息的方式

useEffect(() => {
    axios
      .get('http://127.0.0.1:8081/auth/userinfo', {
      })
      .then((response) => {
        switch (response.data.role) {
          case 'ROLE_STANDARD_USER':
            setRole('ROLE_STANDARD_USER');
            setMenuItems(standardMenuItems);
            break;
          case 'ROLE_ADMIN':
            setRole('ROLE_ADMIN');
            setMenuItems(adminMenuItems);
            break;
          default:
            setRole(null);
            setMenuItems([]);
            break;
        }
      })
      .catch((error) => {
        console.log(error); // handle error
      });

所以我希望工作流程是这样的:

  1. 来自 BFF 服务器的用户请求 /userinfo(前端 oauth2 客户端的后端)
  2. 用户未通过身份验证,因此 BFF 将通过将前端重定向到授权服务器来触发对授权服务器的 /authorize 端点的请求
  3. 用户输入凭据,授权服务器使用授权码重定向回 bff
  4. bff 更进一步,检索访问权限、刷新令牌等,并将它们与会话中的用户凭据一起存储
  5. 用户信息返回给前端

但是这种方法有两个大问题:

  1. CORS 设置
  • 两个服务器(BFF Oauth 客户端和授权服务器)都启用了 cors 以及所有设置(允许标头、允许来源等)

我们有 3 个服务器(域):服务器 A(前端)、服务器 B(BFF)、服务器 C(身份验证服务器)。所以 Server B 正在将 Server A 重定向到 Server C 。在服务器 C 上,由于浏览器设置,请求到达时 origin 设置为 null,这与隐私问题有关。因此,cors 将始终失败,因为它无法使用 null 验证允许的来源。我没有找到任何解决方案

  1. 处理响应的前端问题

CORS 问题的解决方法是将 auth 服务器上的允许来源设置为所有 ( * ),因此在这种情况下 null 来源不再重要,但现在还有另一个问题。 BFF 应该将前端重定向到 auth 服务器,这意味着前端应该出现一个登录页面以输入凭据,但实际情况是,在 axios 请求的响应中,此重定向以 html 形式出现,并且我不知道如何进一步处理它以便让用户输入凭据。

我正在尝试找出前端和 BFF 之间的工作流程,以便以某种方式检索用户角色或正确的身份验证方式。

reactjs spring oauth-2.0 spring-oauth2 spring-authorization-server
© www.soinside.com 2019 - 2024. All rights reserved.