Spring Authorization Server的使用

本文档介绍了如何使用SpringAuthorizationServer搭建一个OAuth2授权服务器,包括授权码和客户端凭证流程,以及配置JWT、JWK和授权服务的相关细节。通过实例展示了授权码的获取、刷新令牌、撤销令牌等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景

Spring Security 5中,现在已经不提供了 授权服务器 的配置,但是 授权服务器 在我们平时的开发过程中用的还是比较多的。不过 Spring 官方提供了一个 由Spring官方主导,社区驱动的授权服务 spring-authorization-server,目前已经到了 0.1.2 的版本,不过该项目还是一个实验性的项目,不可在生产环境中使用,此处来使用项目搭建一个简单的授权服务器。

二、前置知识

1、了解 oauth2 协议、流程。可以参考阮一峰的这篇文章
2、JWT、JWS、JWK的概念

JWT:指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的。
JWS:指的是签过名的JWT,即拥有签名的JWT。
JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。

三、需求

1、 完成授权码(authorization-code)流程。

最安全的流程,需要用户的参与。

2、 完成客户端(client credentials)流程。

没有用户的参与,一般可以用于内部系统之间的访问,或者系统间不需要用户的参与。

3、简化模式在新的 spring-authorization-server 项目中已经被弃用了。
4、刷新令牌。
5、撤销令牌。
6、查看颁发的某个token信息。
7、查看JWK信息。
8、个性化JWT token,即给JWT token中增加额外信息。

完成案例:
张三通过QQ登录的方式来登录优快云网站。
登录后,优快云就可以获取到QQ颁发的token,优快云网站拿着token就可以获取张三在QQ资源服务器上的 个人信息 了。

角色分析
张三: 用户即资源拥有者
优快云:客户端
QQ:授权服务器
个人信息: 即用户的资源,保存在资源服务器中

四、核心代码编写

1、引入授权服务器依赖

<dependency>
    <groupId>org.springframework.security.experimental</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.1.2</version>
</dependency>

2、创建授权服务器用户

张三通过QQ登录的方式来登录优快云网站。

此处完成用户张三的创建,这个张三是授权服务器的用户,此处即QQ服务器的用户。

@EnableWebSecurity
public class DefaultSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin();
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 此处创建用户,张三。
    @Bean
    UserDetailsService users() {
        UserDetails user = User.builder()
                .username("zhangsan")
                .password(passwordEncoder().encode("zhangsan123"))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

3、创建授权服务器和客户端

张三通过QQ登录的方式来登录优快云网站。

此处完成QQ授权服务器和客户端优快云的创建。

package com.huan.study.authorization.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

/**
 * 认证服务器配置
 *
 * @author huan.fu 2021/7/12 - 下午2:08
 */
@Configuration
public class AuthorizationConfig {

    @Autowired
    private PasswordEncoder passwordEncoder;

	/**
     * 个性化 JWT token
     */
    class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

        @Override
        public void customize(JwtEncodingContext context) {
            // 添加一个自定义头
            context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
        }
    }

    /**
     * 定义 Spring Security 的拦截器链
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 设置jwt token个性化
        http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());
        // 授权服务器配置
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        return http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer)
                .and()
                .formLogin()
                .and()
                .build();
    }

    /**
     * 创建客户端信息,可以保存在内存和数据库,此处保存在数据库中
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id 需要唯一
                .clientId("csdn")
                // 客户端密码
                .clientSecret(passwordEncoder.encode("csdn123"))
                // 可以基于 basic 的方式和授权服务器进行认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                // 授权码
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 刷新token
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 客户端模式
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 密码模式
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                // 简化模式,已过时,不推荐
                .authorizationGrantType(AuthorizationGrantType.IMPLICIT)
                // 重定向url
                .redirectUri("https://www.baidu.com")
                // 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
                .scope("user.userInfo")
                .scope("user.photos")
                .clientSettings(clientSettings -> {
                    // 是否需要用户确认一下客户端需要获取用户的哪些权限
                    // 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
                    clientSettings.requireUserConsent(true);
                })
                .tokenSettings(tokenSettings -> {
                    // accessToken 的有效期
                    tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
                    // refreshToken 的有效期
                    tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
                    // 是否可重用刷新令牌
                    tokenSettings.reuseRefreshTokens(true);
                })
                .build();

        JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) {
            jdbcRegisteredClientRepository.save(registeredClient);
        }

        return jdbcRegisteredClientRepository;
    }

    /**
     * 保存授权信息,授权服务器给我们颁发来token,那我们肯定需要保存吧,由这个服务来保存
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);

        class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
            public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
                super(registeredClientRepository);
                getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
                this.setLobHandler(new DefaultLobHandler());
            }
        }

        CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =
                new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);

        authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);
        return authorizationService;
    }

    /**
     * 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息,此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 对JWT进行签名的 加解密密钥
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        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);
    }

    /**
     * 配置一些断点的路径,比如:获取token、授权端点 等
     */
    @Bean
    public ProviderSettings providerSettings() {
        return new ProviderSettings()
                // 配置获取token的端点路径
                .tokenEndpoint("/oauth2/token")
                // 发布者的url地址,一般是本系统访问的根路径
                // 此处的 qq.com 需要修改我们系统的 host 文件
                .issuer("http://qq.com:8080");
    }
}

注意⚠️:
1、需要将 qq.com 在系统的 host 文件中与 127.0.0.1 映射起来。
2、因为客户端信息、授权信息(token信息等)保存到数据库,因此需要将表建好。
客户端和授权信息表
3、详细信息看上方代码的注释

五、测试

从上方的代码中可知:

资源所有者:张三 用户名和密码为:zhangsan/zhangsan123
客户端信息:优快云 clientId和clientSecret:csdn/csdn123
授权服务器地址: qq.com
clientSecret 的值不可泄漏给客户端,必须保存在服务器端。

1、授权码流程

1、获取授权码

http://qq.com:8080/oauth2/authorize?client_id=csdn&response_type=code&redirect_uri=https://www.baidu.com&scope=user.userInfo user.userInfo

client_id=csdn:表示客户端是谁
response_type=code:表示返回授权码
scope=user.userInfo user.userInfo:获取多个权限以空格分开
redirect_uri=https://www.baidu.com:跳转请求,用户同意或拒绝后

2、根据授权码获取token

 curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'

Authorization: 携带具体的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 表示采用的方式是授权码
code=xxx:上一步获取到的授权码

3、流程演示

在这里插入图片描述

2、根据刷新令牌获取token

curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'

在这里插入图片描述

3、客户端模式

此模式下,没有用户的参与,只有客户端和授权服务器之间的参与。

curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/token?grant_type=client_credentials'

客户端模式

4、撤销令牌

curl -i -X POST \
 'http://qq.com:8080/oauth2/revoke?token=令牌'

5、查看token 的信息

curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/introspect?token=XXX'

token详情

6、查看JWK信息

curl -i -X GET \
 'http://qq.com:8080/oauth2/jwks'

JWK信息

六、完整代码

https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server

七、参考地址

1、https://github.com/spring-projects-experimental/spring-authorization-server

### Spring Authorization Server 1.4 Documentation and Usage Guide Spring Authorization Server 是一个用于构建 OAuth 2.0 和 OpenID Connect 提供者的框架。它基于 Spring Security 构建,旨在提供灵活且安全的身份验证和授权解决方案。以下是有关 Spring Authorization Server 1.4 的文档和使用指南的关键点: #### 官方文档 官方文档提供了详细的说明,涵盖了如何配置、扩展以及集成 Spring Authorization Server 到项目中的各个方面[^5]。开发者可以通过访问 [Spring Official Documentation](https://docs.spring.io/spring-authorization-server/docs/current/reference/html/) 获取最新版本的详细信息。 #### Maven 配置 为了在项目中引入 Spring Authorization Server,可以将其作为依赖项添加到 `pom.xml` 文件中。以下是一个典型的 Maven 配置示例: ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.4.0</version> </dependency> ``` 此依赖会自动拉取必要的组件来支持 OAuth 2.0 授权服务的功能实现[^6]。 #### 核心功能概述 Spring Authorization Server 支持多种 OAuth 2.0 流程,包括但不限于: - **Authorization Code Flow**: 这是最常用的流程之一,适用于客户端应用需要获取用户的资源权限场景。 - **Client Credentials Flow**: 主要用于机器间通信,例如微服务之间的调用。 - **Resource Owner Password Flow**: 虽然不推荐广泛使用,但在某些特定情况下仍然适用。 - **Refresh Token Support**: 允许通过刷新令牌重新获得新的访问令牌而无需再次登录。 这些核心功能均已在 Spring Authorization Server 中得到良好支持,并可通过自定义方式进行调整以满足不同业务需求[^7]。 #### 示例代码片段 下面展示了一个简单的 Spring Boot 应用程序启动类,其中包含了基本的安全性和授权服务器配置: ```java import org.springframework.context.annotation.Bean; import org.springframework.security.config.Customizer; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; public class AuthorizationServerConfig { @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId("client-id") .clientId("my-client") .clientSecret("{noop}secret") .scope("read", "write") .redirectUri("http://localhost:8080/login/oauth2/code/my-client") .tokenSettings(Customizer.withDefaults()) .build(); return new InMemoryRegisteredClientRepository(registeredClient); } } ``` 以上代码展示了如何注册一个基础的客户端实例并存储于内存之中[^8]。 #### 数据库持久化 默认情况下,Spring Authorization Server 使用内存数据库保存授权相关信息。如果希望切换至关系型数据库,则需额外增加 JPA 或 JDBC 相关依赖,并修改相应的实体映射逻辑[^9]。 ---
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值