我在我们的Sping Boot auth服务器上获得了一个基本的Vue.js应用程序用于身份验证。下面是执行完整登录的代码(重定向到/login,请求到token端点等):
<template>
<div class="h-screen w-screen flex flex-col items-center justify-center">
<p class="text-center mb-5">You need to be logged in<br>to access the admin dashboard.</p>
<button @click.prevent="startAuthFlow" class="bg-blue-500 text-white h-8 w-36 rounded shadow">Login</button>
</div>
</template>
<script setup>
import { config
} from "./../common/config.js";
import { parseQueryString, generateRandomString } from "./../utils/authHelpers.js";
import router from './../router';
import { useAuthStore } from './../stores/auth.js';
const auth = useAuthStore();
if (auth.access_token) {
router.push({ name: 'home', replace: true });
}
const startAuthFlow = () => {
console.log("Starting auth flow...");
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
console.log(state);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&state=" + encodeURIComponent(state)
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri);
// Redirect to the authorization server
window.location = url;
};
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Base64 encode client credentials
const base64Credentials = btoa(config.client_id + ':' + config.client_secret);
// Build the token URL
var url = config.token_endpoint
+ "?grant_type=authorization_code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&code=" + q.code
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri);
// Send POST request to token endpoint to retrieve access token
fetch(url, {
method: "POST",
headers: {
"Authorization": "Basic " + base64Credentials,
'Content-Type': 'application/x-www-form-urlencoded',
}
}).then(response => response.json())
.then(result => {
console.log(result);
// Extracting tokens from result
const { access_token, refresh_token } = result;
// Save login to pinia and tokens to cookies
auth.login({ access_token, refresh_token, user: null });
router.push({ name: 'home', replace: true });
})
.catch(error => console.log('error', error));
}
// Clean up local storage
localStorage.removeItem("pkce_state");
}
</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停用的浏览器中,我得到以下标题:
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
型
下面是我的SecurityConfig
文件以及CorsConfig
:
@Configuration
public class
CorsConfig {
private static final Logger logger = LoggerFactory.getLogger(CorsConfig.class);
/**
* Cors configuration
*/
@Bean(name="corsConfigurationSource")
CorsConfigurationSource corsConfigurationSource() {
logger.info("Creating corsConfigurationSource bean");
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(
"http://localhost:5173",
"http://192.168.2.144:5173"
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(List.of(
"Authorization",
"Content-Type",
"Accept",
"Origin",
"X-Requested-With"
));
configuration.setExposedHeaders(List.of(
"Cache-Control",
"Content-Language",
"Content-Type",
"Expires",
"Last-Modified",
"Pragma"
));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
型
和
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// claim names used in the bearer token
private static final String ROLES_CLAIM = "user-authorities";
private static final String SCOPES_CLAIM = "scope";
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Bean
@Order(1)
public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
logger.info("Creating corsFilter bean");
return new CorsFilter(corsConfigurationSource);
}
/**
* Configures the authorization server endpoints.
*/
@Bean
@Order(2)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, RegisteredClientRepository clientRepository) throws Exception {
logger.info("Creating authorizationServerSecurityFilterChain bean");
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.registeredClientRepository(clientRepository) // autowired from ClientConfig.java
.oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
/**
* Secures pages used to log in, log out, register etc.
* Sets custom login menu.
*/
@Bean
@Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/admin/**")));
logger.info("Creating defaultSecurityFilterChain bean");
http.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers(new AntPathRequestMatcher("/register")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/recover/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/error/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/css/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/js/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/favicon.ico")).permitAll()
.anyRequest().authenticated());
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
// set custom login form
http.formLogin(form -> {
form.loginPage("/login");
form.permitAll();
});
http.logout(conf -> {
// default logout url
conf.logoutSuccessHandler(logoutSuccessHandler());
});
// Temp disable CSRF
http.csrf(AbstractHttpConfigurer::disable);
http.cors(AbstractHttpConfigurer::disable);
return http.build();
}
/**
* Secures admin endpoints with a bearer token. Does not use session authentication.
*/
@Bean
@Order(4)
public SecurityFilterChain adminResourceFilterChain(HttpSecurity http) throws Exception {
logger.info("Creating adminResourceFilterChain bean");
// handle out custom endpoints in this filter chain
http.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers(new AntPathRequestMatcher("/admin/**")).hasRole("ADMIN")
.anyRequest().authenticated());
http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
// Temp disable CSRF
http.csrf(AbstractHttpConfigurer::disable);
http.cors(AbstractHttpConfigurer::disable);
return http.build();
}
// ...
型
我尝试了很多不同的方法:
- 更改了过滤器链(例如,在每个安全Bean的顶部包含CORS配置,而不是
Order(1)
) - 接着,我们一个接一个地实现了CORS Tutorial。
- 尝试手动设置请求头 (这不是长期解决方案)。
- 尝试模式匹配以暂时允许每个传入请求(不起作用,因此这显然是CORS配置本身的问题)
1条答案
按热度按时间zwghvu4y1#
SsinglePageAapplications(Angular,React,Vue.js等)以及移动的应用程序不应该是OAuth2客户端。这样的客户端是“公共”客户端,现在不鼓励这样做。
您的Vue应用程序应该通过使用OAuth2登录的BFF上的会话进行保护,并使用包含访问令牌的授权头来替换会话cookie。实现这一点的最简单方法可能是使用
spring-cloud-gateway
和TokenRelay
过滤器以及spring-boot-starter-oauth2-client
和oauth2Login
,就像我在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配置的需求: