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:
  • 开发实践
    cover: https://cover.png
    feature: false

1. 授权服务器

目前 Spring 生态中的 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,就会支持如下协议端点:

  • 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 类,创建一个新的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 最小配置那节里注入 UserDetailsService 这个 Bean 使用的是 InMemoryUserDetailsManager,表示内存模式,这里使用 JdbcUserDetailsManager 表示 JDBC 模式

而这两个类都属于 UserDetailsManager 接口的实现类,并且后续我们需要使用到 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);
    }

    /**
     * 令牌的发放记录, 对应 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 创建数据库表

一共包括 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)
);

创建完成后的数据库表如下:

OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client

1.4 自定义登录和授权页面

在项目 resource 目录下创建一个 templates 文件夹,然后创建 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>

创建 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>

创建 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 实战总结_凡 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

保存,发送后,会给我们返回 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

保存,发送后,会给我们返回新的 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

保存,发送后,会给我们返回 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 实战总结_凡 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 为自定义的统一结果返回类,这里的返回信息自定义即可

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不主动创建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

保存,发送后,即可获取资源 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>

创建 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();

        // 发起请求
        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 可以发现直接进入了 index.html,无需再次登录

OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client

可以发现我们访问时是带了一个 JESSEIONID 的,用户登录后,会在认证服务器和客户端都保存 session 信息

OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client文章来源地址https://www.toymoban.com/news/detail-475409.html

到了这里,关于OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 搭建spring security oauth2认证授权服务器

    下面是在spring security项目的基础上搭建spring security oauth2认证授权服务器 spring security oauth2认证授权服务器主要需要以下依赖 Spring Security对OAuth2默认可访问端点 ​/oauth/authorize​ ​​:申请授权码code,涉及类​ ​AuthorizationEndpoint​ ​ ​/oauth/token​ ​​:获取令牌token,涉及类​

    2024年01月21日
    浏览(57)
  • Spring Boot OAuth2 认证服务器搭建及授权码认证演示

    本篇使用JDK版本是1.8,需要搭建一个OAuth 2.0的认证服务器,用于实现各个系统的单点登录。 这里选择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日
    浏览(49)
  • 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日
    浏览(38)
  • 【Spring Authorization Server 系列】(一)入门篇,快速搭建一个授权服务器

    官方主页:https://spring.io/projects/spring-authorization-server Spring Authorization Server 是一个框架,提供了 OAuth 2.1 和 OpenID Connect 1.0 规范以及其他相关规范的实现。 它建立在 Spring Security 之上,为构建 OpenID Connect 1.0 Identity Providers 和 OAuth2 Authorization Server 产品提供安全、轻量级和可定制

    2024年02月16日
    浏览(51)
  • oauth2-resource-server授权配置介绍

    当了解这篇文章授权服务器后,对授权服务器有一定的认识,那么授权服务器生成token后,该怎么用呢,这就涉及到资源服务器,现在给大家简单介绍实现过程。 2.1 基于官网配置 首先先配置 issuer-uri ,这里指向是授权服务器的地址 关于过滤器链的配置: 资源服务器将使用

    2024年02月12日
    浏览(31)
  • Spring OAuth2 授权服务器配置详解

    首先要创建一个Spring Boot Servlet Web项目,这个不难就不赘述了。集成 Spring Authorization Server 需要引入: OAuth2.0 Client 客户端需要注册到授权服务器并持久化, Spring Authorization Server 提供了 JDBC 实现,参见 JdbcRegisteredClientRepository 。为了演示方便这里我采用了H2数据库,需要以下依

    2024年04月13日
    浏览(45)
  • 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日
    浏览(53)
  • 授权码 + PKCE 模式|OIDC & OAuth2.0 认证协议最佳实践系列【03】

    ​ 在上一篇文章中,我们介绍了 OIDC 授权码模式(点击下方链接查看), 本次我们将重点围绕 授权码 + PKCE 模式(Authorization Code With PKCE)进行介绍 ,从而让你的系统快速具备接入用户认证的标准体系。 OIDC OAuth2.0 认证协议最佳实践系列 02 - 授权码模式(Authorization Code)接

    2024年02月01日
    浏览(92)
  • SpringCloud整合spring security+ oauth2+Redis实现认证授权

    在微服务构建中,我们一般用一个父工程来通知管理依赖的各种版本号信息。父工程pom文件如下: 在SpringCloud微服务体系中服务注册中心是一个必要的存在,通过注册中心提供服务的注册和发现。具体细节可以查看我之前的博客,这里不再赘述。我们开始构建一个eureka注册中

    2024年02月06日
    浏览(58)
  • Spring Cloud Gateway 整合OAuth2.0 实现统一认证授权

    Spring Cloud Gateway 整合OAuth2.0 实现统一认证授权 GateWay——向其他服务传递参数数据 https://blog.csdn.net/qq_38322527/article/details/126530849 @EnableAuthorizationServer Oauth2ServerConfig 验证签名 网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来 接下来搭建网

    2024年02月13日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包