Spring Security 如何在Sping Boot Auth Server上正确配置CORS?

e4yzc0pl  于 2023-11-19  发布在  Spring
关注(0)|答案(1)|浏览(149)

我在我们的Sping Boot auth服务器上获得了一个基本的Vue.js应用程序用于身份验证。下面是执行完整登录的代码(重定向到/login,请求到token端点等):

  1. <template>
  2. <div class="h-screen w-screen flex flex-col items-center justify-center">
  3. <p class="text-center mb-5">You need to be logged in<br>to access the admin dashboard.</p>
  4. <button @click.prevent="startAuthFlow" class="bg-blue-500 text-white h-8 w-36 rounded shadow">Login</button>
  5. </div>
  6. </template>
  7. <script setup>
  8. import { config
  9. } from "./../common/config.js";
  10. import { parseQueryString, generateRandomString } from "./../utils/authHelpers.js";
  11. import router from './../router';
  12. import { useAuthStore } from './../stores/auth.js';
  13. const auth = useAuthStore();
  14. if (auth.access_token) {
  15. router.push({ name: 'home', replace: true });
  16. }
  17. const startAuthFlow = () => {
  18. console.log("Starting auth flow...");
  19. // Create and store a random "state" value
  20. var state = generateRandomString();
  21. localStorage.setItem("pkce_state", state);
  22. console.log(state);
  23. // Build the authorization URL
  24. var url = config.authorization_endpoint
  25. + "?response_type=code"
  26. + "&client_id=" + encodeURIComponent(config.client_id)
  27. + "&state=" + encodeURIComponent(state)
  28. + "&redirect_uri=" + encodeURIComponent(config.redirect_uri);
  29. // Redirect to the authorization server
  30. window.location = url;
  31. };
  32. // Handle the redirect back from the authorization server and
  33. // get an access token from the token endpoint
  34. var q = parseQueryString(window.location.search.substring(1));
  35. // Check if the server returned an error string
  36. if (q.error) {
  37. alert("Error returned from authorization server: " + q.error);
  38. }
  39. // If the server returned an authorization code, attempt to exchange it for an access token
  40. if (q.code) {
  41. // Verify state matches what we set at the beginning
  42. if (localStorage.getItem("pkce_state") != q.state) {
  43. alert("Invalid state");
  44. } else {
  45. // Base64 encode client credentials
  46. const base64Credentials = btoa(config.client_id + ':' + config.client_secret);
  47. // Build the token URL
  48. var url = config.token_endpoint
  49. + "?grant_type=authorization_code"
  50. + "&client_id=" + encodeURIComponent(config.client_id)
  51. + "&code=" + q.code
  52. + "&redirect_uri=" + encodeURIComponent(config.redirect_uri);
  53. // Send POST request to token endpoint to retrieve access token
  54. fetch(url, {
  55. method: "POST",
  56. headers: {
  57. "Authorization": "Basic " + base64Credentials,
  58. 'Content-Type': 'application/x-www-form-urlencoded',
  59. }
  60. }).then(response => response.json())
  61. .then(result => {
  62. console.log(result);
  63. // Extracting tokens from result
  64. const { access_token, refresh_token } = result;
  65. // Save login to pinia and tokens to cookies
  66. auth.login({ access_token, refresh_token, user: null });
  67. router.push({ name: 'home', replace: true });
  68. })
  69. .catch(error => console.log('error', error));
  70. }
  71. // Clean up local storage
  72. localStorage.removeItem("pkce_state");
  73. }
  74. </script>

字符串
当使用一个浏览器与停用的安全功能(不检查CORS),这完全正常工作。登录以及其他请求。
现在,当使用普通浏览器时,重定向到/login工作正常。但是当向token端点发送请求时,出现以下错误:
访问"http://localhost:8081/oauth2/token?grant_type=authorization_code&client_id=core-server&code= Ps19 gIUUePThDLr 15 xX 0 U-UWEMD 0 HgHfyAO CcZjGmfXUqED 80 GOMLBykuldNrL 7 k23 dxEklcP 49 hX_kGigKsZjcFCTS 93 xU 7 kwdwAIm 6-e IcIk_ayN5i0mZPLeg_bDYx&redirect_uri =http%3A%2F%2Flocalhost%3A5173%2F'从原点' http://localhost:CORS策略已阻止“服务器5173”:对预检请求的响应未通过访问控制检查:请求的资源上不存在“Occup-Control-Allow-Origin”标头。如果不透明响应满足您的需求,请将请求的模式设置为“no-cors”以在禁用CORS的情况下获取资源。
在CORS停用的浏览器中,我得到以下标题:

  1. Vary: Origin
  2. Vary: Access-Control-Request-Method
  3. Vary: Access-Control-Request-Headers


下面是我的SecurityConfig文件以及CorsConfig

  1. @Configuration
  2. public class
  3. CorsConfig {
  4. private static final Logger logger = LoggerFactory.getLogger(CorsConfig.class);
  5. /**
  6. * Cors configuration
  7. */
  8. @Bean(name="corsConfigurationSource")
  9. CorsConfigurationSource corsConfigurationSource() {
  10. logger.info("Creating corsConfigurationSource bean");
  11. CorsConfiguration configuration = new CorsConfiguration();
  12. configuration.setAllowedOrigins(List.of(
  13. "http://localhost:5173",
  14. "http://192.168.2.144:5173"
  15. ));
  16. configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
  17. configuration.setAllowCredentials(true);
  18. configuration.setAllowedHeaders(List.of(
  19. "Authorization",
  20. "Content-Type",
  21. "Accept",
  22. "Origin",
  23. "X-Requested-With"
  24. ));
  25. configuration.setExposedHeaders(List.of(
  26. "Cache-Control",
  27. "Content-Language",
  28. "Content-Type",
  29. "Expires",
  30. "Last-Modified",
  31. "Pragma"
  32. ));
  33. configuration.setMaxAge(3600L);
  34. UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  35. source.registerCorsConfiguration("/**", configuration);
  36. return source;
  37. }
  38. }


  1. @Configuration
  2. @EnableWebSecurity
  3. public class SecurityConfig {
  4. // claim names used in the bearer token
  5. private static final String ROLES_CLAIM = "user-authorities";
  6. private static final String SCOPES_CLAIM = "scope";
  7. private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
  8. @Bean
  9. @Order(1)
  10. public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
  11. logger.info("Creating corsFilter bean");
  12. return new CorsFilter(corsConfigurationSource);
  13. }
  14. /**
  15. * Configures the authorization server endpoints.
  16. */
  17. @Bean
  18. @Order(2)
  19. public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, RegisteredClientRepository clientRepository) throws Exception {
  20. logger.info("Creating authorizationServerSecurityFilterChain bean");
  21. OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
  22. http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
  23. .registeredClientRepository(clientRepository) // autowired from ClientConfig.java
  24. .oidc(Customizer.withDefaults());
  25. http.exceptionHandling((exceptions) -> exceptions
  26. .defaultAuthenticationEntryPointFor(
  27. new LoginUrlAuthenticationEntryPoint("/login"),
  28. new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
  29. )
  30. );
  31. http.oauth2ResourceServer((resourceServer) -> resourceServer
  32. .jwt(Customizer.withDefaults()));
  33. http.csrf(AbstractHttpConfigurer::disable);
  34. return http.build();
  35. }
  36. /**
  37. * Secures pages used to log in, log out, register etc.
  38. * Sets custom login menu.
  39. */
  40. @Bean
  41. @Order(3)
  42. public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
  43. http.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/admin/**")));
  44. logger.info("Creating defaultSecurityFilterChain bean");
  45. http.authorizeHttpRequests((authorize) ->
  46. authorize
  47. .requestMatchers(new AntPathRequestMatcher("/register")).permitAll()
  48. .requestMatchers(new AntPathRequestMatcher("/recover/**")).permitAll()
  49. .requestMatchers(new AntPathRequestMatcher("/error/**")).permitAll()
  50. .requestMatchers(new AntPathRequestMatcher("/css/**")).permitAll()
  51. .requestMatchers(new AntPathRequestMatcher("/js/**")).permitAll()
  52. .requestMatchers(new AntPathRequestMatcher("/favicon.ico")).permitAll()
  53. .anyRequest().authenticated());
  54. http.oauth2ResourceServer((resourceServer) -> resourceServer
  55. .jwt(Customizer.withDefaults()));
  56. // set custom login form
  57. http.formLogin(form -> {
  58. form.loginPage("/login");
  59. form.permitAll();
  60. });
  61. http.logout(conf -> {
  62. // default logout url
  63. conf.logoutSuccessHandler(logoutSuccessHandler());
  64. });
  65. // Temp disable CSRF
  66. http.csrf(AbstractHttpConfigurer::disable);
  67. http.cors(AbstractHttpConfigurer::disable);
  68. return http.build();
  69. }
  70. /**
  71. * Secures admin endpoints with a bearer token. Does not use session authentication.
  72. */
  73. @Bean
  74. @Order(4)
  75. public SecurityFilterChain adminResourceFilterChain(HttpSecurity http) throws Exception {
  76. logger.info("Creating adminResourceFilterChain bean");
  77. // handle out custom endpoints in this filter chain
  78. http.authorizeHttpRequests((authorize) ->
  79. authorize
  80. .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN")
  81. .anyRequest().authenticated());
  82. http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
  83. http.oauth2ResourceServer((resourceServer) -> resourceServer
  84. .jwt(Customizer.withDefaults()));
  85. // Temp disable CSRF
  86. http.csrf(AbstractHttpConfigurer::disable);
  87. http.cors(AbstractHttpConfigurer::disable);
  88. return http.build();
  89. }
  90. // ...


我尝试了很多不同的方法:

  • 更改了过滤器链(例如,在每个安全Bean的顶部包含CORS配置,而不是Order(1)
  • 接着,我们一个接一个地实现了CORS Tutorial
  • 尝试手动设置请求头 (这不是长期解决方案)
  • 尝试模式匹配以暂时允许每个传入请求(不起作用,因此这显然是CORS配置本身的问题)
zwghvu4y

zwghvu4y1#

SsinglePageAapplications(Angular,React,Vue.js等)以及移动的应用程序不应该是OAuth2客户端。这样的客户端是“公共”客户端,现在不鼓励这样做。

您的Vue应用程序应该通过使用OAuth2登录的BFF上的会话进行保护,并使用包含访问令牌的授权头来替换会话cookie。实现这一点的最简单方法可能是使用spring-cloud-gatewayTokenRelay过滤器以及spring-boot-starter-oauth2-clientoauth2Login,就像我在this tutorial中所做的那样。本教程中的前端使用Angular,但是登录和注销是普通的Typsecript代码,很容易移植到React或Vue。
除了通过会话保护前端之外,网关还可以消除对大部分CORS配置的需求:从浏览器的Angular 来看,通过网关路由的所有请求都具有相同的起源(网关)。
在链接的教程中:

  • 所有以/ui/开始的路径发送到网关(比如说https://localhost:8080 ui/ui/)的请求都被路由到为UI资产服务的对象(在Angular dev服务器的情况下,类似于https://localhost:4200 ui/ui/,但也可能是Vue dev服务器,包含任何内容的NGINX示例,等等)
  • 所有通过网关的请求,其路径以/bff/v1/开始,都被路由到资源服务器(类似于https://localhost:/bff/v1//)

在上面的配置中,如果用户将其浏览器指向https://localhost:web 8080脚本/ui/,并且如果SPA被配置为将REST请求发送到https://localhost:web 8080脚本/bff/v1/ui *,则从浏览器的Angular 来看,对UI和API的请求都以https://localhost:web 8080脚本 * 为来源。
仍然在本教程中,授权服务器不通过网关路由,需要一些CORS配置才能允许以网关为源的请求。原因是,在使用OAuth2时,最常见的情况是您对SingleSignOn感兴趣:在不同的应用程序之间共享相同的授权服务器,以保存用户在使用同一浏览器时多次认证的需要,这需要使用相同的cookie,因此使用相同的主机和端口(浏览器在https://oidc.c4-soft.com上联系授权服务器),无论BFF示例如何(而不是像教程BFF中的https://localhost:8080/auth这样的东西)。
如果使用网关作为OAuth2机密客户端,您可以删除对CORS配置的需求:

  • 从资源服务器(如果您通过网关路由UI和REST请求)
  • 从授权服务器(如果您通过网关路由UI和授权服务器,但代价是失去SSO)
展开查看全部

相关问题