国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

這篇具有很好參考價值的文章主要介紹了OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。


title: OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client
date: 2023-03-27 01:41:26
tags:

  • OAuth2.0
  • Spring Authorization Server
    categories:
  • 開發(fā)實踐
    cover: https://cover.png
    feature: false

1. 授權服務器

目前 Spring 生態(tài)中的 OAuth2 授權服務器是 Spring Authorization Server,原先的 Spring Security OAuth 已經停止更新

1.1 引入依賴

這里的 spring-security-oauth2-authorization-server 用的是 0.4.0 版本,適配 JDK 1.8,Spring Boot 版本為 2.7.7

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
</dependencies>

1.2 配置類

可以參考官方的 Samples:spring-authorization-server/samples

1.2.1 最小配置

官網最小配置 Demo 地址:Getting Started

官網最小配置如下,通過添加該配置類,啟動項目,這就能夠完成 OAuth2 的授權

@Configuration
public class SecurityConfig {

	@Bean 
	@Order(1)
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
			throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
			.oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
		http
			// Redirect to the login page when not authenticated from the
			// authorization endpoint
			.exceptionHandling((exceptions) -> exceptions
				.authenticationEntryPoint(
					new LoginUrlAuthenticationEntryPoint("/login"))
			)
			// Accept access tokens for User Info and/or Client Registration
			.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);

		return http.build();
	}

	@Bean 
	@Order(2)
	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
			throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			// Form login handles the redirect to the login page from the
			// authorization server filter chain
			.formLogin(Customizer.withDefaults());

		return http.build();
	}

	@Bean 
	public UserDetailsService userDetailsService() {
		UserDetails userDetails = User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();

		return new InMemoryUserDetailsManager(userDetails);
	}

	@Bean 
	public RegisteredClientRepository registeredClientRepository() {
		RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
				.clientId("messaging-client")
				.clientSecret("{noop}secret")
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
				.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
				.redirectUri("http://127.0.0.1:8080/authorized")
				.scope(OidcScopes.OPENID)
				.scope(OidcScopes.PROFILE)
				.scope("message.read")
				.scope("message.write")
				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
				.build();

		return new InMemoryRegisteredClientRepository(registeredClient);
	}

	@Bean 
	public JWKSource<SecurityContext> jwkSource() {
		KeyPair keyPair = generateRsaKey();
		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
		RSAKey rsaKey = new RSAKey.Builder(publicKey)
				.privateKey(privateKey)
				.keyID(UUID.randomUUID().toString())
				.build();
		JWKSet jwkSet = new JWKSet(rsaKey);
		return new ImmutableJWKSet<>(jwkSet);
	}

	private static KeyPair generateRsaKey() { 
		KeyPair keyPair;
		try {
			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
			keyPairGenerator.initialize(2048);
			keyPair = keyPairGenerator.generateKeyPair();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
		return keyPair;
	}

	@Bean 
	public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
		return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
	}

	@Bean 
	public AuthorizationServerSettings authorizationServerSettings() {
		return AuthorizationServerSettings.builder().build();
	}

}

在上面的 Demo 里,將所有配置都寫在了一個配置類 SecurityConfig 里,實際上 Spring Authorization Server 還提供了一種實現最小配置的默認配置形式,就是通過 OAuth2AuthorizationServerConfiguration 這個類,源碼如下:

@Configuration(proxyBeanMethods = false)
public class OAuth2AuthorizationServerConfiguration {

	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
		applyDefaultSecurity(http);
		return http.build();
	}

	// @formatter:off
	public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
		OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
				new OAuth2AuthorizationServerConfigurer();
		RequestMatcher endpointsMatcher = authorizationServerConfigurer
				.getEndpointsMatcher();

		http
			.requestMatcher(endpointsMatcher)
			.authorizeRequests(authorizeRequests ->
				authorizeRequests.anyRequest().authenticated()
			)
			.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
			.apply(authorizationServerConfigurer);
	}
	// @formatter:on

	public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
		Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
		jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
		jwsAlgs.addAll(JWSAlgorithm.Family.EC);
		jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
		JWSKeySelector<SecurityContext> jwsKeySelector =
				new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);
		jwtProcessor.setJWSKeySelector(jwsKeySelector);
		// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead
		jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
		});
		return new NimbusJwtDecoder(jwtProcessor);
	}

	@Bean
	RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
		RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
		postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build());
		return postProcessor;
	}

}

這里注入一個叫做 authorizationServerSecurityFilterChain 的 bean,其實對比一下可以看出,這和最小配置的實現基本是相同的。有了這個 bean,就會支持如下協(xié)議端點:

  • OAuth2 Authorization endpoint
  • OAuth2 Token endpoint
  • OAuth2 Token Introspection endpoint
  • OAuth2 Token Revocation endpoint
  • OAuth2 Authorization Server Metadata endpoint
  • JWK Set endpoint
  • OpenID Connect 1.0 Provider Configuration endpoint
  • OpenID Connect 1.0 UserInfo endpoint

接下來使用 OAuth2AuthorizationServerConfiguration 這個類來實現一個 Authorization Server,將 Spring Security 和 Authorization Server 的配置分開,Spring Security 使用 SecurityConfig 類,創(chuàng)建一個新的Authorization Server 配置類 AuthorizationServerConfig

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.2.2 ServerSecurityConfig

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ServerSecurityConfig {

    @Resource
    private DataSource dataSource;

    /**
     * Spring Security 的過濾器鏈,用于 Spring Security 的身份認證
     */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                        // 配置放行的請求
                        .antMatchers("/api/**", "/login").permitAll()
                        // 其他任何請求都需要認證
                        .anyRequest().authenticated()
                )
                // 設置登錄表單頁面
                .formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login"));

        return http.build();
    }
  
//    @Bean
//    public UserDetailsService userDetailsService() {
//        return new JdbcUserDetailsManager(dataSource);
//    }

    @Bean
    UserDetailsManager userDetailsManager() {
        return new JdbcUserDetailsManager(dataSource);
    }
}

Spring Authorization Server 默認是支持內存和 JDBC 兩種存儲模式的,內存模式只適合簡單的測試,所以這里使用 JDBC 存儲模式。在 1.2.1 最小配置那節(jié)里注入 UserDetailsService 這個 Bean 使用的是 InMemoryUserDetailsManager,表示內存模式,這里使用 JdbcUserDetailsManager 表示 JDBC 模式

而這兩個類都屬于 UserDetailsManager 接口的實現類,并且后續(xù)我們需要使用到 userDetailsManager.createUser(userDetails) 方法來添加用戶,因此這里需要注入 UserDetailsManager 這個 Bean,由于返回的都是 JdbcUserDetailsManager,因此可以注釋掉 UserDetailsService 這個 Bean 的注入

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.2.3 AuthorizationServerConfig

該類部分配置可以參照前面提到的 OAuth2AuthorizationServerConfiguration 類來配置,同樣使用 JDBC 存儲模式

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 定義授權服務配置器
        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
        configurer
                // 自定義授權頁面
                .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
                // Enable OpenID Connect 1.0, 啟用 OIDC 1.0
                .oidc(Customizer.withDefaults());

        // 獲取授權服務器相關的請求端點
        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();

        http
                // 攔截對授權服務器相關端點的請求
                .requestMatcher(endpointsMatcher)
                // 攔載到的請求需要認證
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                // 忽略掉相關端點的 CSRF(跨站請求): 對授權端點的訪問可以是跨站的
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                // 訪問端點時表單登錄
                .formLogin()
                .and()
                // 應用授權服務器的配置
                .apply(configurer);

        return http.build();
    }

    /**
     * 注冊客戶端應用, 對應 oauth2_registered_client 表
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    /**
     * 令牌的發(fā)放記錄, 對應 oauth2_authorization 表
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 把資源擁有者授權確認操作保存到數據庫, 對應 oauth2_authorization_consent 表
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 加載 JWT 資源, 用于生成令牌
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }

        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();

        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    /**
     * JWT 解碼
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * AuthorizationServerS 的相關配置
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

1.3 創(chuàng)建數據庫表

一共包括 5 個表,其中 Spring Security 相關的有 2 個表,user 和 authorities,用戶表和權限表,該表的建表 SQL 在

org\springframework\security\core\userdetails\jdbc\users.ddl

SQL 可能會有一些問題,根據自己使用的數據庫進行更改

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

Spring authorization Server 有 3 個表,建表 SQL 在:

org\springframework\security\oauth2\server\authorization\oauth2-authorization-consent-schema.sql

org\springframework\security\oauth2\server\authorization\oauth2-authorization-schema.sql

org\springframework\security\oauth2\server\authorization\client\oauth2-registered-client-schema.sql

CREATE TABLE oauth2_authorization_consent (
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorities varchar(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);
/*
IMPORTANT:
    If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
    as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
    id varchar(100) NOT NULL,
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorization_grant_type varchar(100) NOT NULL,
    authorized_scopes varchar(1000) DEFAULT NULL,
    attributes blob DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorization_code_value blob DEFAULT NULL,
    authorization_code_issued_at timestamp DEFAULT NULL,
    authorization_code_expires_at timestamp DEFAULT NULL,
    authorization_code_metadata blob DEFAULT NULL,
    access_token_value blob DEFAULT NULL,
    access_token_issued_at timestamp DEFAULT NULL,
    access_token_expires_at timestamp DEFAULT NULL,
    access_token_metadata blob DEFAULT NULL,
    access_token_type varchar(100) DEFAULT NULL,
    access_token_scopes varchar(1000) DEFAULT NULL,
    oidc_id_token_value blob DEFAULT NULL,
    oidc_id_token_issued_at timestamp DEFAULT NULL,
    oidc_id_token_expires_at timestamp DEFAULT NULL,
    oidc_id_token_metadata blob DEFAULT NULL,
    refresh_token_value blob DEFAULT NULL,
    refresh_token_issued_at timestamp DEFAULT NULL,
    refresh_token_expires_at timestamp DEFAULT NULL,
    refresh_token_metadata blob DEFAULT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE oauth2_registered_client (
    id varchar(100) NOT NULL,
    client_id varchar(100) NOT NULL,
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret varchar(200) DEFAULT NULL,
    client_secret_expires_at timestamp DEFAULT NULL,
    client_name varchar(200) NOT NULL,
    client_authentication_methods varchar(1000) NOT NULL,
    authorization_grant_types varchar(1000) NOT NULL,
    redirect_uris varchar(1000) DEFAULT NULL,
    scopes varchar(1000) NOT NULL,
    client_settings varchar(2000) NOT NULL,
    token_settings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

創(chuàng)建完成后的數據庫表如下:

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.4 自定義登錄和授權頁面

在項目 resource 目錄下創(chuàng)建一個 templates 文件夾,然后創(chuàng)建 login.html 和 consent.html,登錄頁面的配置在 1.2.2 中配置好了,授權頁面的配置在 1.2.3 中配置好了

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

登錄頁面 login.html

<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Spring Security Example</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
    <form class="form-signin" method="post" th:action="@{/login}">
        <div th:if="${param.error}" class="alert alert-danger" role="alert">
            用戶名或密碼無效
        </div>
        <div th:if="${param.logout}" class="alert alert-success" role="alert">
            您已注銷
        </div>
        <h2 class="form-signin-heading">登錄</h2>
        <p>
            <label for="username" class="sr-only">用戶名</label>
            <input type="text" id="username" name="username" class="form-control" placeholder="用戶名" required autofocus>
        </p>
        <p>
            <label for="password" class="sr-only">密 碼</label>
            <input type="password" id="password" name="password" class="form-control" placeholder="密 碼" required>
        </p>
        <button class="btn btn-lg btn-primary btn-block" type="submit">登錄</button>
        <a class="btn btn-light btn-block bg-white" href="/oauth2/authorization/github-idp" role="link" style="text-transform: none;">
            <img width="24" style="margin-right: 5px;" alt="Sign in with GitHub" src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" />
            使用Github登錄
        </a>
    </form>
</div>
</body>
</html>

創(chuàng)建 LoginConroller,用于跳轉到 login.html 頁面

@Controller
public class LoginController {

	@GetMapping("/login")
	public String login() {
		return "login";
	}
}

授權頁面 consent.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
          integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <title>Custom consent page - Consent required</title>
    <style>
        body {
            background-color: aliceblue;
        }
    </style>
	<script>
		function cancelConsent() {
			document.consent_form.reset();
			document.consent_form.submit();
		}
	</script>
</head>
<body>
<div class="container">
    <div class="py-5">
        <h1 class="text-center text-primary">應用程序權限</h1>
    </div>
    <div class="row">
        <div class="col text-center">
            <p>
                應用程序
                <span class="font-weight-bold text-primary" th:text="${clientId}"></span>
                想要訪問您的帳戶
                <span class="font-weight-bold" th:text="${principalName}"></span>
            </p>
        </div>
    </div>
    <div class="row pb-3">
        <div class="col text-center"><p>上述應用程序請求以下權限<br>如果您批準,請查看這些并同意</p></div>
    </div>
    <div class="row">
        <div class="col text-center">
            <form name="consent_form" method="post" th:action="@{/oauth2/authorize}">
                <input type="hidden" name="client_id" th:value="${clientId}">
                <input type="hidden" name="state" th:value="${state}">

                <div th:each="scope: ${scopes}" class="form-group form-check py-1">
                    <input class="form-check-input"
                           type="checkbox"
                           name="scope"
                           th:value="${scope.scope}"
                           th:id="${scope.scope}">
                    <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
                    <p class="text-primary" th:text="${scope.description}"></p>
                </div>

                <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已向上述應用授予以下權限:</p>
                <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
                    <input class="form-check-input"
                           type="checkbox"
                           th:id="${scope.scope}"
                           disabled
                           checked>
                    <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
                    <p class="text-primary" th:text="${scope.description}"></p>
                </div>

                <div class="form-group pt-3">
                    <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
                        提交授權
                    </button>
                </div>
                <div class="form-group">
                    <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
                        取消
                    </button>
                </div>
            </form>
        </div>
    </div>
    <div class="row pt-4">
        <div class="col text-center">
            <p>
                <small>
                    Your consent to provide access is required.
                    <br/>If you do not approve, click Cancel, in which case no information will be shared with the app.
                </small>
            </p>
        </div>
    </div>
</div>
</body>
</html>

創(chuàng)建 AuthorizationConsentController,用于跳轉到 consent.html 頁面

@Controller
public class AuthorizationConsentController {
	private final RegisteredClientRepository registeredClientRepository;
	private final OAuth2AuthorizationConsentService authorizationConsentService;

	public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
			OAuth2AuthorizationConsentService authorizationConsentService) {
		this.registeredClientRepository = registeredClientRepository;
		this.authorizationConsentService = authorizationConsentService;
	}

	@GetMapping(value = "/oauth2/consent")
	public String consent(Principal principal, Model model,
			@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
			@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
			@RequestParam(OAuth2ParameterNames.STATE) String state) {

		// 要批準的范圍和以前批準的范圍
		Set<String> scopesToApprove = new HashSet<>();
		Set<String> previouslyApprovedScopes = new HashSet<>();
		// 查詢 clientId 是否存在
		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
		// 查詢當前的授權許可
		OAuth2AuthorizationConsent currentAuthorizationConsent =
				this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());

		// 已授權范圍
		Set<String> authorizedScopes;
		if (currentAuthorizationConsent != null) {
			authorizedScopes = currentAuthorizationConsent.getScopes();
		} else {
			authorizedScopes = Collections.emptySet();
		}
		for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
			if (OidcScopes.OPENID.equals(requestedScope)) {
				continue;
			}
			// 如果已授權范圍包含了請求范圍,則添加到以前批準的范圍的 Set, 否則添加到要批準的范圍
			if (authorizedScopes.contains(requestedScope)) {
				previouslyApprovedScopes.add(requestedScope);
			} else {
				scopesToApprove.add(requestedScope);
			}
		}

		model.addAttribute("clientId", clientId);
		model.addAttribute("state", state);
		model.addAttribute("scopes", withDescription(scopesToApprove));
		model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
		model.addAttribute("principalName", principal.getName());

		return "consent";
	}

	private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
		Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
		for (String scope : scopes) {
			scopeWithDescriptions.add(new ScopeWithDescription(scope));
		}
		return scopeWithDescriptions;
	}

	public static class ScopeWithDescription {
		private static final String DEFAULT_DESCRIPTION = "未知范圍 - 我們無法提供有關此權限的信息, 請在授予此權限時謹慎";
		private static final Map<String, String> scopeDescriptions = new HashMap<>();
		static {
			scopeDescriptions.put(
					OidcScopes.PROFILE,
					"此應用程序將能夠讀取您的個人資料信息"
			);
			scopeDescriptions.put(
					"message.read",
					"此應用程序將能夠讀取您的信息"
			);
			scopeDescriptions.put(
					"message.write",
					"此應用程序將能夠添加新信息, 它還可以編輯和刪除現有信息"
			);
			scopeDescriptions.put(
					"other.scope",
					"這是范圍描述的另一個范圍示例"
			);
		}

		public final String scope;
		public final String description;

		ScopeWithDescription(String scope) {
			this.scope = scope;
			this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
		}
	}
}

1.5 ServerController

用于添加用戶信息和客戶端信息,這里的 passwordEncoder 使用 BCryptPasswordEncoder 進行加解密,{bcrypt} 表示加密,{noop} 表示明文

@RestController
public class ServerController {

    @Resource
    private UserDetailsManager userDetailsManager;

    @GetMapping("/api/addUser")
    public String addUser() {
        UserDetails userDetails = User.builder().passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s))
                .username("fan")
                .password("fan")
                .roles("ADMIN")
                .build();

        userDetailsManager.createUser(userDetails);
        return "添加用戶成功";
    }

    @Resource
    private RegisteredClientRepository registeredClientRepository;

    @GetMapping("/api/addClient")
    public String addClient() {
        // JWT(Json Web Token)的配置項:TTL、是否復用refreshToken等等
        TokenSettings tokenSettings = TokenSettings.builder()
                // 令牌存活時間:2小時
                .accessTokenTimeToLive(Duration.ofHours(2))
                // 令牌可以刷新,重新獲取
                .reuseRefreshTokens(true)
                // 刷新時間:30天(30天內當令牌過期時,可以用刷新令牌重新申請新令牌,不需要再認證)
                .refreshTokenTimeToLive(Duration.ofDays(30))
                .build();
        // 客戶端相關配置
        ClientSettings clientSettings = ClientSettings.builder()
                // 是否需要用戶授權確認
                .requireAuthorizationConsent(true)
                .build();

        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客戶端ID和密碼
                .clientId("messaging-client")
//                .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))
                .clientSecret("{noop}secret")
                // 授權方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授權模式(授權碼模式)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 刷新令牌(授權碼模式)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 回調地址:授權服務器向當前客戶端響應時調用下面地址, 不在此列的地址將被拒絕, 只能使用IP或域名,不能使用 localhost
                .redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")
                // OIDC 支持
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                // 授權范圍(當前客戶端的授權范圍)
                .scope("message.read")
                .scope("message.write")
                // JWT(Json Web Token)配置項
                .tokenSettings(tokenSettings)
                // 客戶端配置項
                .clientSettings(clientSettings)
                .build();

        registeredClientRepository.save(registeredClient);
        return "添加客戶端信息成功";
    }
}

1.6 YAML 配置

配置數據庫連接信息

server:
  port: 9000

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

1.7 測試

完整目錄結構如下:

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.7.1 添加用戶和客戶端信息

啟動項目,訪問 http://127.0.0.1:9000/api/addUser

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

查詢數據庫 users 和 authorities 表,已有用戶和權限信息

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

訪問 http://127.0.0.1:9000/api/addClient

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

查詢數據庫 oauth2_registered_client 表,已有客戶端信息

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.7.2 授權碼模式獲取令牌

有關 OAuth2.0 的相關知識可見:OAuth2.0 實戰(zhàn)總結_凡 223 的博客

訪問 http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,這里的 127.0.0.1:8000 其實為客戶端地址,后面講到客戶端時,客戶端的地址就為 8000

  • response_type:授權類型,code 為授權碼模式
  • client_id:客戶端 ID,即前面注冊客戶端的時候定義的
  • scope:請求的權限范圍
  • redirect_uri:回調地址,也是前面注冊客戶端的時候定義的

未登錄,會跳轉到登錄頁面

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

輸入前面添加的用戶信息,用戶名和密碼,然后會跳轉到授權頁面

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

選擇是否授予權限,這里勾選后,點擊提交,會跳轉到回調地址,即 127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,由于這個地址還沒有對應的服務,無法訪問,但我們暫時需要的是地址欄的 code

http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=z_3O1lEdxVsd2fn8_uKA481pO9caGd0N4x_Vbt0deuMA77sDis6fhMJkf2_9uM4KGYzLzv7ujbXZ2JAdg0ACyMapR38jnJruG2iz2XBgptKrru-IJobGVa6NTicgvCZ7

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

打開接口測試工具,這里我使用的是 Apifox,使用表單格式,包含三個參數

  • grant_type:授權類型,authorization_code 表示授權碼模式
  • code:即授權碼,上面地址欄里返回給我們的 code 部分,復制到這里,code 使用一次就會失效
  • redirect_uri:回調地址,與前面的一致。圖中的地址忘記修改了,注意和前面請求 code 時寫的回調地址一致,即 http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,后面有類似問題同樣修改

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

然后設置 Auth,Postman 里是 Authorization,選擇 Basic Auth 類型,用戶名密碼則為注冊客戶端時的 client_id 和 clientSecret,客戶端 ID 和密鑰

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

保存,發(fā)送后,會給我們返回 access_token 和 refresh_token

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

將 access_token 復制到 JSON Web Tokens - jwt.io 網站,解析后可以看到 JWT 的信息,包括客戶端 ID,權限范圍,服務器地址等

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.7.3 授權碼模式刷新令牌

在前面返回了 access_token 和 refresh_token,access_token 包含了授權信息,refresh_token 則是用來重新獲取 access_token,同樣是表單類型,包含兩個參數

  • grant_type:refresh_token 表示刷新令牌
  • refresh_token:即前面獲取到的 refresh_token 的值

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

Auth 信息與前面一致

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

保存,發(fā)送后,會給我們返回新的 access_token 和 refresh_token,refresh_token 使用一次就會失效

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.7.4 客戶端模式

同樣使用表單格式,grant_type 值為 client_credentials

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

Auth 與前面一致

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

保存,發(fā)送后,會給我們返回 access_token,沒有 refresh_token。因為在授權碼模式中的 access_token 是我們通過授權碼 code 換來的,而授權碼 code 是我們請求后授權得到的,為了不用每次獲取 access_token 都需要重新請求授權,所以使用 refresh_token 來重新獲取 access_token,refresh_token 和 access_token 都有過期時間,refresh_token 過期時間比 access_token 長

而客戶端模式可以直接獲取 access_token,所以也就不需要 refresh_token 了

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

1.7.5 OIDC

有關 OIDC 的相關知識同樣可見:OAuth2.0 實戰(zhàn)總結_凡 223 的博客

在前面 1.2.3 的配置和 1.5 的注冊客戶端時,已經支持了 OIDC,這里直接訪問:http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc

這里的 scope 必須包含 openid

得到授權碼 code

http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=NjvT1z3msYRsjvPPM4LP4EmlyBUixsKes_J6osSB3VAugXEKmyUappvtrmTWp7s_iQzoJsD8xOE3gUXawhMixL0fu2HC6UJv8CeZyCB-d2oiu4NnCO9uJcK1MXOm4poU

然后通過授權碼 code 換取令牌,可以看到除了 access_token 和 refresh_token 外,還返回了一個 id_token

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

解析這個 id_token,信息如下,是我們的身份認證信息

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

再通過 refresh_token 重新獲取令牌,同樣也給我們返回了 id_token

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

通過 access_token,獲取 OIDC 的用戶端點

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

這里的 sub 就是用戶的標志。在 1.2.3 的配置中,對于 OIDC 使用的是默認配置

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

我們也可以增加自定義信息,修改后的配置如下,其他配置不變

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 定義授權服務配置器
        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
        configurer
                // 自定義授權頁面
                .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
                // Enable OpenID Connect 1.0, 啟用 OIDC 1.0
                .oidc(oidcConfigurer -> oidcConfigurer.userInfoEndpoint(userInfoEndpointConfigurer ->
                        userInfoEndpointConfigurer.userInfoMapper(userInfoAuthenticationContext -> {
                            OAuth2AccessToken accessToken = userInfoAuthenticationContext.getAccessToken();
                            Map<String, Object> claims = MapUtil.map(false);

                            claims.put("url", "http://127.0.0.1:9000");
                            claims.put("accessToken", accessToken);
                            claims.put("sub", userInfoAuthenticationContext.getAuthorization().getPrincipalName());

                            return new OidcUserInfo(claims);
                        })));

        // 獲取授權服務器相關的請求端點
        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();

        http
                // 攔截對授權服務器相關端點的請求
                .requestMatcher(endpointsMatcher)
                // 攔載到的請求需要認證
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                // 忽略掉相關端點的 CSRF(跨站請求): 對授權端點的訪問可以是跨站的
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                // 訪問端點時表單登錄
                .formLogin()
                .and()
                // 應用授權服務器的配置
                .apply(configurer);

        return http.build();
    }

    // ... 其他配置不變
}

重啟項目,重新獲取到 access_token,通過 access_token 訪問用戶端點,可以看到我們自定義的信息已經被添加了進來

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

2. 資源服務器

2.1 引入依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
</dependencies>

2.2 YAML 配置

server:
  port: 8001

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9000

2.3 異常處理器

該部分為 Spring Security 相關知識,可見:Spring Security 總結_凡 223 的博客

2.3.1 認證失敗處理器

Response 為自定義的統(tǒng)一結果返回類,這里的返回信息自定義即可

public class UnAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 403, 未授權, 禁止訪問
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        // 返回響應信息
        ServletOutputStream outputStream = response.getOutputStream();
        Response fail = Response.fail(HttpServletResponse.SC_FORBIDDEN,
                "UnAccessDeniedHandler-未授權, 不允許訪問", "uri-" + request.getRequestURI());
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));

        // 關閉流
        outputStream.flush();
        outputStream.close();
    }
}

2.3.2 鑒權失敗處理器


public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (authException instanceof InvalidBearerTokenException) {
            LogUtil.info("Token 登錄失效");
        }

        if (response.isCommitted()) {
            return;
        }

        // 401, 未認證
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setStatus(HttpServletResponse.SC_ACCEPTED);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        // 返回響應信息
        ServletOutputStream outputStream = response.getOutputStream();

        Response fail = Response.fail(HttpServletResponse.SC_UNAUTHORIZED,
                authException.getMessage() + "-UnAuthenticationEntryPoint-認證失敗", "uri-" + request.getRequestURI());
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));
        // 關閉流
        outputStream.flush();
        outputStream.close();
    }
}

2.4 配置類

對資源請求配置了讀、寫、profile 權限

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ResourceServerConfig {

    /**
     * 資源管理器配置
     *
     * @param http
     * @return {@link SecurityFilterChain}
     * @author Fan
     * @since 2023/2/2 9:30
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        UnAuthenticationEntryPoint authenticationEntryPoint = new UnAuthenticationEntryPoint();
        UnAccessDeniedHandler accessDeniedHandler = new UnAccessDeniedHandler();

        http
                // security的session生成策略改為security不主動創(chuàng)建session, 即STALELESS
                // 資源服務不涉及用戶登錄, 僅靠token訪問, 不需要seesion
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        // 對 /resource1 的請求,需要 SCOPE_message.read 權限
                        .antMatchers("/resource1").hasAuthority("SCOPE_message.read")
                        // 對 /resource2 的請求,需要 SCOPE_message.write 權限
                        .antMatchers("/resource2").hasAuthority("SCOPE_message.write")
                        // 對 /resource3 的請求,需要 SCOPE_profile 權限
                        .antMatchers("/resource3").hasAuthority("SCOPE_profile")
                        // 放行請求
                        .antMatchers("/api/**").permitAll()
                        // 其他任何請求都需要認證
                        .anyRequest().authenticated())
                // 異常處理器
                .exceptionHandling(exceptionConfigurer -> exceptionConfigurer
                        // 認證失敗
                        .authenticationEntryPoint(authenticationEntryPoint)
                        // 鑒權失敗
                        .accessDeniedHandler(accessDeniedHandler)
                )
                // 資源服務
                .oauth2ResourceServer(resourceServer -> resourceServer
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                        .jwt());

        return http.build();
    }
}

2.5 Controller

@RestController
public class MessagesController {

	@GetMapping("/resource1")
	public Response getResource1(){
		return Response.success("服務A -> 資源1 -> 讀權限");
	}

	@GetMapping("/resource2")
	public Response getResource2(){
		return Response.success("服務A -> 資源2 -> 寫權限");
	}

	@GetMapping("/resource3")
	public Response resource3(){
		return Response.success("服務A -> 資源3 -> profile 權限");
	}

	@GetMapping("/api/publicResource")
	public Response publicResource() {
		return Response.success("服務A -> 公共資源");
	}
}

2.6 測試

完整目錄結構如下:

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

啟動項目,打開 Apifox,直接請求時,會提示我們認證失敗,即上面認證失敗處理器的響應結果

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

添加 Auth,類型選擇 Bearer Token,Token 的值即為前面獲取到的 access_token 的值

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

保存,發(fā)送后,即可獲取資源 resource1

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

再獲取資源 resource2,提示沒有權限,這里返回的信息即為鑒權失敗處理器的響應信息。因為在我們申請權限的時候只申請了 message.read 權限,同時也只授權了 message.read 權限,而 resource2 需要 message.write 權限,因此鑒權失敗,無法訪問

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

3. 客戶端

3.1 引入依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
</dependencies>

3.2 YAML 配置

server:
  port: 8000

spring:
  application:
    name: messages-client
  security:
    oauth2:
      client:
        registration:
          messaging-client-oidc:
            provider: authorization-server
            client-id: messaging-client
            client-secret: secret
            authorization-grant-type: authorization_code
#            redirect-uri: "127.0.0.1:8000/login/oauth2/code/messaging-client-oidc"
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            scope: openid,message.read,message.write
            client-name: messaging-client-oidc
        provider:
          # 服務提供地址
          authorization-server:
            # issuer-uri 可以簡化下面的配置
            issuer-uri: http://localhost:9000
            # 請求授權碼地址
#            authorization-uri: http://localhost:9000/oauth2/authorize
            # 請求令牌地址
#            token-uri: http://localhost:9000/oauth2/token
            # 用戶資源地址
#            user-info-uri: http://localhost:9000/oauth2/user
            # 用戶資源返回中的一個屬性名
#            user-name-attribute: name
#            user-info-authentication-method: GET

這里的配置要和注冊客戶端時的配置對應上,同一顏色對應,這里使用的是 OIDC,scope 加上了 openid

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

注意:使用 OIDC 是為了使用默認的用戶端點,假如不使用 OIDC 需要自定義用戶端點接口,否則會報如下錯誤

[invalid_user_info_response] An error occurred while attempting to retrieve the UserInfo Resource: 403 : “{“error”:“insufficient_scope”}”

3.3 配置類

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ClientSecurityConfig {

    /**
     * 安全配置
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize ->
                        // 任何請求都需要認證
                        authorize.anyRequest().authenticated()
                )
                // 登錄
//                .oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
                .oauth2Login(Customizer.withDefaults())
                .oauth2Client(Customizer.withDefaults());

        return http.build();
    }
}

3.4 index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
登錄用戶:<span th:text="${user}"></span>
<hr/>
<ul>
    <li><a href="./server/a/resource1">服務A —— 資源1</a></li>
    <li><a href="./server/a/resource2">服務A —— 資源2</a></li>
    <li><a href="./server/a/resource3">服務A —— 資源3</a></li>
    <li><a href="./server/a/publicResource">服務A —— 公共資源</a></li>
</ul>
</body>
</html>

創(chuàng)建 IndexController,跳轉到 index.html

@Controller
public class IndexController {

	@GetMapping("/")
	public String root() {
		return "redirect:/index";
	}

	@GetMapping("/index")
	public String index(Model model) {
		Map<String, Object> map = MapUtil.map(false);

		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		map.put("name", auth.getName());

		Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
		List<? extends GrantedAuthority> authoritiesList = authorities.stream().collect(Collectors.toList());
		map.put("authorities", authoritiesList);

		model.addAttribute("user", JSONUtil.toJsonStr(map));
		return "index";
	}
}

3.5 ResourceController

@RestController
public class ResourceController {

    @GetMapping("/server/a/resource1")
    public String getServerARes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/resource1", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/a/resource2")
    public String getServerARes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/resource2", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/a/resource3")
    public String getServerBRes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/resource3", oAuth2AuthorizedClient);
    }

    @GetMapping("/server/a/publicResource")
    public String getServerBRes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return getServer("http://127.0.0.1:8001/api/publicResource", oAuth2AuthorizedClient);
    }

    /**
     * 綁定token,請求微服務
     */
    private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        LogUtil.info("getServer");
        // 獲取 access_token
        String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();

        // 發(fā)起請求
        Mono<String> stringMono = WebClient.builder()
                .defaultHeader("Authorization", "Bearer " + tokenValue)
                .build()
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);

        return stringMono.block();
    }
}

3.6 測試

完整目錄結構如下:

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

啟動項目,訪問 127.0.0.1:8000,未登錄會直接跳轉到登錄頁面

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

輸入用戶名密碼,登錄后進入授權頁面

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

選擇想要授予的權限,這里勾選 read 權限,點擊提交,跳轉到我們的首頁 index.html

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

將上面 user 的 JSON 信息格式化一下如下,可以看到就是我們的認證和權限信息

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

點擊訪問 服務A -> 資源1

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

點擊訪問 服務A -> 資源2,無法訪問

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

這是因為之前授權時只給了 read 權限,而資源 2 需要 write 權限,可以看到報了 403 異常,這里可以定義一個異常處理類,來返回對應的信息,而不是白頁

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

我們關閉當前頁面新開一個頁面,再次訪問 127.0.0.1:8000 可以發(fā)現直接進入了 index.html,無需再次登錄

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client

可以發(fā)現我們訪問時是帶了一個 JESSEIONID 的,用戶登錄后,會在認證服務器和客戶端都保存 session 信息

OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client文章來源地址http://www.zghlxwxcb.cn/news/detail-475409.html

到了這里,關于OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 搭建spring security oauth2認證授權服務器

    搭建spring security oauth2認證授權服務器

    下面是在spring security項目的基礎上搭建spring security oauth2認證授權服務器 spring security oauth2認證授權服務器主要需要以下依賴 Spring Security對OAuth2默認可訪問端點 ?/oauth/authorize? ??:申請授權碼code,涉及類? ?AuthorizationEndpoint? ? ?/oauth/token? ??:獲取令牌token,涉及類?

    2024年01月21日
    瀏覽(20)
  • Spring Boot OAuth2 認證服務器搭建及授權碼認證演示

    Spring Boot OAuth2 認證服務器搭建及授權碼認證演示

    本篇使用JDK版本是1.8,需要搭建一個OAuth 2.0的認證服務器,用于實現各個系統(tǒng)的單點登錄。 這里選擇Spring Boot+Spring Security + Spring Authorization Server 實現,具體的版本選擇如下: Spirng Boot 2.7.14 , Spring Boot 目前的最新版本是 3.1.2,在官方的介紹中, Spring Boot 3.x 需要JDK 17及以上的

    2024年02月15日
    瀏覽(23)
  • spring cloud、gradle、父子項目、微服務框架搭建---spring secuity oauth2、mysql 授權(九)

    spring cloud、gradle、父子項目、微服務框架搭建---spring secuity oauth2、mysql 授權(九)

    https://preparedata.blog.csdn.net/article/details/120062997 新建兩個服務 1.授權服務 端口號:11007 2.資源服務 端口號:11004 資源服務可以是訂單服務、用戶服務、商品服務等等 當然這兩個服務也可以合并到一起, 依次順序AuthorizationServerConfiguration、ResourceServerConfig、WebSecurityConfiguration;

    2024年02月10日
    瀏覽(27)
  • 【Spring Authorization Server 系列】(一)入門篇,快速搭建一個授權服務器

    【Spring Authorization Server 系列】(一)入門篇,快速搭建一個授權服務器

    官方主頁:https://spring.io/projects/spring-authorization-server Spring Authorization Server 是一個框架,提供了 OAuth 2.1 和 OpenID Connect 1.0 規(guī)范以及其他相關規(guī)范的實現。 它建立在 Spring Security 之上,為構建 OpenID Connect 1.0 Identity Providers 和 OAuth2 Authorization Server 產品提供安全、輕量級和可定制

    2024年02月16日
    瀏覽(26)
  • oauth2-resource-server授權配置介紹

    當了解這篇文章授權服務器后,對授權服務器有一定的認識,那么授權服務器生成token后,該怎么用呢,這就涉及到資源服務器,現在給大家簡單介紹實現過程。 2.1 基于官網配置 首先先配置?issuer-uri ,這里指向是授權服務器的地址 關于過濾器鏈的配置: 資源服務器將使用

    2024年02月12日
    瀏覽(25)
  • Spring OAuth2 授權服務器配置詳解

    Spring OAuth2 授權服務器配置詳解

    首先要創(chuàng)建一個Spring Boot Servlet Web項目,這個不難就不贅述了。集成 Spring Authorization Server 需要引入: OAuth2.0 Client 客戶端需要注冊到授權服務器并持久化, Spring Authorization Server 提供了 JDBC 實現,參見 JdbcRegisteredClientRepository 。為了演示方便這里我采用了H2數據庫,需要以下依

    2024年04月13日
    瀏覽(20)
  • Spring Security—OAuth2 客戶端認證和授權

    關于 JWT Bearer 客戶端認證的進一步詳情,請參考OAuth 2.0客戶端認證和授權許可的?JSON Web Token (JWT)簡介。 JWT Bearer 客戶端認證的默認實現是? NimbusJwtClientAuthenticationParametersConverter ,它是一個? Converter ,通過在? client_assertion ?參數中添加簽名的JSON Web Token(JWS)來定制令牌請求

    2024年02月08日
    瀏覽(25)
  • 授權碼 + PKCE 模式|OIDC & OAuth2.0 認證協(xié)議最佳實踐系列【03】

    授權碼 + PKCE 模式|OIDC & OAuth2.0 認證協(xié)議最佳實踐系列【03】

    ? 在上一篇文章中,我們介紹了?OIDC?授權碼模式(點擊下方鏈接查看), 本次我們將重點圍繞 授權碼 + PKCE 模式(Authorization Code With PKCE)進行介紹 ,從而讓你的系統(tǒng)快速具備接入用戶認證的標準體系。 OIDC OAuth2.0 認證協(xié)議最佳實踐系列 02 - 授權碼模式(Authorization Code)接

    2024年02月01日
    瀏覽(20)
  • Spring Cloud Gateway 整合OAuth2.0 實現統(tǒng)一認證授權

    Spring Cloud Gateway 整合OAuth2.0 實現統(tǒng)一認證授權 GateWay——向其他服務傳遞參數數據 https://blog.csdn.net/qq_38322527/article/details/126530849 @EnableAuthorizationServer Oauth2ServerConfig 驗證簽名 網關服務需要RSA的公鑰來驗證簽名是否合法,所以認證服務需要有個接口把公鑰暴露出來 接下來搭建網

    2024年02月13日
    瀏覽(23)
  • SpringCloud整合spring security+ oauth2+Redis實現認證授權

    SpringCloud整合spring security+ oauth2+Redis實現認證授權

    在微服務構建中,我們一般用一個父工程來通知管理依賴的各種版本號信息。父工程pom文件如下: 在SpringCloud微服務體系中服務注冊中心是一個必要的存在,通過注冊中心提供服務的注冊和發(fā)現。具體細節(jié)可以查看我之前的博客,這里不再贅述。我們開始構建一個eureka注冊中

    2024年02月06日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包