【OAuth2】11-认识SpringAuthorization Server

一、Spring Authorization Server

Spring Authorization Server 起初是一个社区驱动的项目,在 Spring 的实验项目中启动,由 Spring Security 团队领导,其目的主要是为 Spring 社区提供 OAuth 2.0 授权服务器支持,并最终取代 Spring Security OAuth 。该项目使用 ZenHub 来确定功能路线图的优先次序,并帮助组织项目计划。

自 2020 年 4 月发布 Spring Authorization Server 以来,其已实现的功能已经足够为大部分 OAuth 2.1 授权框架提供支持。

Spring Authorization Server 是一个框架,提供了OAuth 2.1和OpenID Connect 1.0规范以及其他相关规范的实现。它建立在Spring Security之上,为构建 OpenID Connect 1.0 Identity Providers 和 OAuth2 Authorization Server 产品提供安全、轻量级和可定制的基础。
此外,在已经发布到 Spring Authorization Server 0.3.1 版本,这也是由 Spring 的新政策支持的正式生产版本。
官网地址

1、 功能列表在这里插入图片描述

2、 依赖坐标

-我现在练习的是一下版本,最新的是0.3.1

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

3、环境要求

虽然目前官方并没有明确说明,但是从源代码很容易分析出来Spring Authorization Server的环境要求。

  • Java 8及以上。
  • Spring Boot 2.5.9及以上。
  • Spring Security 5.5.4及以上。
  • Servlet Web环境,将来不排除对Reactive Web的支持。

二、Spring Authorization Server初体验

分支: springauthserver

1、环境依赖:

像OAuth2 Client、Resource Server一样,Spring Authorization Server也是以插件的形式接入Spring Security的体系中。下面列举了目前必备的环境依赖:

    <dependencies>
        <!--  actuator 指标监控  非必须 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--  spring security starter 必须  -->
        <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>
            <version>0.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
        <!--      orm  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

        <!-- spring mvc  servlet web  必须  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--   lombok 插件 非必须       -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 测试   -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2、授权服务器过滤器链

OAuth2授权服务器专门处理OAuth2客户端的授权请求流程,授权端点、Token端点、用户信息端点等等都需要对应的过滤器支持,这些过滤器由Spring Authorization Server中的OAuth2AuthorizationServerConfigurer负责初始化和配置。我们只需要定义一个优先级最高的过滤器链,把授权服务器配置类初始化并激活即可。

 @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        // TODO 你可以根据需求对authorizationServerConfigurer进行一些个性化配置
        RequestMatcher authorizationServerEndpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        // ①
        http.requestMatcher(authorizationServerEndpointsMatcher)
                .authorizeRequests().anyRequest().authenticated()
                .and()
                // ②忽略掉相关端点的csrf
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers(authorizationServerEndpointsMatcher))
                // 开启form登录
                .formLogin()
                .and()
                // ③应用 授权服务器的配置
                .apply(authorizationServerConfigurer);
        return http.build();
    }

上面是一个基本的配置,关键的步骤为:

  • 配置拦截授权服务器相关的请求端点。
  • 由于是接口调用,同时关闭相关端点的CSRF功能。
  • 将配置类加入HttpSecurity激活配置。

3、客户端的注册和持久化管理

按照OAuth2协议,所有的OAuth2客户端都应该在授权服务器中进行信息注册。你去申请接入第三方开放平台,都要提交一些信息,第三方平台审核通过后会把一些OAuth2客户端信息发给你,这些信息你不会陌生,大部分都包含在OAuth2客户端类库的OAuth2ClientProperties.Registration中,对应Spring Authorization Server授权服务器的实体为RegisteredClient
在这里插入图片描述

这些属性多数在前面的章节中已经介绍了,redirect_uri变成了复数以适应多个OAuth2客户端,另外redirect_uri还有一些隐含规则和操作
,相关源码:
在这里插入图片描述
这里简单总结一个要点:

  • redirect_uri不能有锚点(fragment),比如微信DEMO中携带了锚点#wechat_redirect,这种事实上是不符合OAuth2规范的。
  • redirect_uri的host不能为null或者localhost,这一点非常重要。
  • 如果redirect_uri的host不是环回地址,必须注册到授权服务器,精确匹配到URI字符串。
  • 如果redirect_uri的host是环回地址,可以在调用时切换端口port。
  • 如果OAuth2授权服务器是Spring Authorization Server,目前必须严格按照这个规则配置redirect_uri。

4、 ClientSettings

该OAuth2客户端的一些规则配置,包括:

  • REQUIRE_PROOF_KEY 授权码授权流程中是否需要对密钥进行质询和验证,默认false。当为true时,开启授权码PKCE支持 RFC7636。
  • REQUIRE_AUTHORIZATION_CONSENT 客户端请求授权时是否添加同意授权选项。
  • JWK_SET_URL 这个参见Spring Security中的JOSE类库中相关的描述。
  • TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM 为private_key_jwt和client_secret_jwt声明JWS签名算法。只能用于令牌端点对客户端进行身份验证环节。
private_key_jwt和client_secret_jwt参见ClientAuthenticationMethod。

5、TokenSettings

注册OAuth2客户端时对该客户端令牌的通用规则配置,包含了:

  • ACCESS_TOKEN_TIME_TO_LIVE 访问令牌生存时间,默认5分钟。
  • REUSE_REFRESH_TOKENS 是否可以复用刷新令牌,默认true。
  • ID_TOKEN_SIGNATURE_ALGORITHM OIDC ID Token使用的签名算法,默认RS256
你可以通过TokenSettings.withSettings添加额外的自定义属性或者覆盖已有的属性。

我们来初始化一个OAuth2客户端,这里我们使用的客户端授权方法ClientAuthenticationMethod是client_secret_basic,因为之前对应的basic已经不建议使用了:

  private RegisteredClient createRegisteredClient(final String id) {
        return RegisteredClient.withId(UUID.randomUUID().toString())
//               客户端ID和密码
                .clientId("felord")
//               此处为了避免频繁启动重复写入仓库
                .id(id)
//                client_secret_basic    客户端需要存明文   服务器存密文
                .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder()
                        .encode("secret"))
//                名称 可不定义
                .clientName("felord")
//                授权方法
                .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:8082/login/oauth2/code/test-client-oidc")
                .redirectUri("http://127.0.0.1:8082/authorized")
                .redirectUri("http://127.0.0.1:8082/login/oauth2/code/test")
                .redirectUri("http://127.0.0.1:8082/test/bar")
                .redirectUri("https://baidu.com")
//                OIDC支持
                .scope(OidcScopes.OPENID)
//                其它Scope
                .scope("message.read")
                .scope("userinfo")
                .scope("message.write")
//                JWT的配置项 包括TTL  是否复用refreshToken等等
                .tokenSettings(TokenSettings.builder().build())
//                配置客户端相关的配置项,包括验证密钥或者 是否需要授权页面
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true).build())
                .build();
    }

上面注册的OAuth2客户端信息需要持久化到数据库,RegisteredClientRepository接口抽象了对RegisteredClient的持久化操作,这里我们直接启用内置的JDBC实现以代替默认的内存实现:

   @SneakyThrows
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        //         每次都会初始化  生产的话 只初始化JdbcRegisteredClientRepository
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        // TODO 生产上 注册客户端需要使用接口 不应该采用下面的方式
        // only@test begin
        final String id = "10000";
        RegisteredClient registeredClient = registeredClientRepository.findById(id);
        if (registeredClient == null) {
            registeredClient = this.createRegisteredClient(id);
            //这里为了测试,我们在初始化JdbcRegisteredClientRepository的时候保存了一个OAuth2客户端信息。
            registeredClientRepository.save(registeredClient);
        }
        // only@test end
        return registeredClientRepository;
    }

6、授权状态信息持久化

资源拥有者的OAuth2授权状态信息OAuth2Authorization也需要持久化管理,Spring Authorization Server提供了OAuth2AuthorizationService来负责这个工作,我们同样需要启用内置的JDBC实现以代替默认的内存实现:

    @Bean
    public OAuth2AuthorizationService authorizationService(
            JdbcTemplate jdbcTemplate, 
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate,
                registeredClientRepository);
    }

授权确认状态持久化
如果该客户端配置ClientSettings开启了授权确认REQUIRE_AUTHORIZATION_CONSENT ,授权确认的信息也要持久化管理,需要启用内置的JDBC实现以代替默认的内存实现:

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(
            JdbcTemplate jdbcTemplate,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, 
                registeredClientRepository);
    }
OAuth2客户端注册到授权服务器的注册信息中配置了授权确认功能才有用。

7、JWK源配置

授权服务器公私钥都需要,参考Spring Security中的JOSE类库中的方法,结合Spring Authorization Server提供的方案,我们只需要定义一个JWKSource类型的Spring Bean即可:

/**
     * 加载JWK资源
     *
     * @return the jwk source
     */
    @SneakyThrows
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        //TODO 这里优化到配置
        // jks classpath路径
        String path = "jose.jks";
        // key alias
        String alias = "jose";
        // password
        String pass = "test.cn";

        ClassPathResource resource = new ClassPathResource(path);
        KeyStore jks = KeyStore.getInstance("jks");
        char[] pin = pass.toCharArray();
        jks.load(resource.getInputStream(), pin);
        RSAKey rsaKey = RSAKey.load(jks, alias, pin);

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

到这里就配置完了。启动项目,访问下面的issue端点:

http://localhost:9000/.well-known/oauth-authorization-server

将返回授权服务器的元信息:

{
    "issuer": "http://localhost:9000",
    "authorization_endpoint": "http://localhost:9000/oauth2/authorize",
    "token_endpoint": "http://localhost:9000/oauth2/token",
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "jwks_uri": "http://localhost:9000/oauth2/jwks",
    "response_types_supported": [
        "code"
    ],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token"
    ],
    "revocation_endpoint": "http://localhost:9000/oauth2/revoke",
    "revocation_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "introspection_endpoint": "http://localhost:9000/oauth2/introspect",
    "introspection_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ]
}

这些配置是提供给OAuth2客户端的,里面也有不少的端点,比如jwks_uri你可以访问一下,看看能否获取公钥JWK。

注册一个用户

OAuth2客户端请求授权跳转到授权服务器,需要一个授权服务器用户登录认证并同意授权。我们在Spring Authorization Server授权服务器中临时指定一个测试用户test,密码为123456:

@Bean
    UserDetailsService users() {
        UserDetails user = User.builder()
                .username("test")
                .password("123456")
                .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

这个用户就是OAuth2中的资源拥有者(Resource Owner)。

附数据库DDL脚本

Spring Authorization Server的类库内置了数据库DDL脚本,在org/springframework/security/oauth2/server/authorization下,分别是

  • oauth2-authorization-schema.sql
  • oauth2-authorization-consent-schema.sql
  • oauth2-registered-client-schema.sql
    你可以手动或者借助于spring.sql.init系列命令进行初始化。

三、Spring Authorization Server客户端

Spring Authorization Server的服务器已经在上面中搭建好了,并注册了一个OAuth2客户端,本篇将利用这个注册的客户端实现HttpSecurity.oauth2Client功能。

1、OAuth2客户端配置

1.1、配置文件

先配置OAuth2客户端的配置文件,这里要对照着Spring Authorization Server中注册的那个OAuth2客户端。这里抄过来对照:

private RegisteredClient createRegisteredClient(final String id) {
        return RegisteredClient.withId(UUID.randomUUID().toString())
//               客户端ID
                .clientId("felord")
//               此处为了避免频繁启动重复写入仓库
                .id(id)
//                client_secret_basic 模式下的密码  在客户端需要存明文 在授权服务器存密文
                .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder()
                              .encode("secret"))
//                名称可不定义
                .clientName("felord")
//                授权方法
                .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:8082/login/oauth2/code/test-client-oidc")
                .redirectUri("http://127.0.0.1:8082/authorized")
                .redirectUri("http://127.0.0.1:8082/login/oauth2/code/felord")
                .redirectUri("http://127.0.0.1:8082/test/bar")
                .redirectUri("https://baidu.com")
//                OIDC支持
                .scope(OidcScopes.OPENID)
//                其它Scope
                .scope("message.read")
                .scope("userinfo")
                .scope("message.write")
//                JWT的配置项 包括TTL  是否复用refreshToken等等
                .tokenSettings(TokenSettings.builder().build())
//                配置客户端相关的配置项,包括验证密钥或者 是否需要授权页面
                .clientSettings(ClientSettings.builder()
                                .requireAuthorizationConsent(true).build())
                .build();
    }

1.2、对应的yaml配置:

spring:
  security:
    oauth2:
      client:
        registration:
          test:
            client-id: felord
            client-secret: secret
            redirect-uri:  'http://127.0.0.1:8082/test/bar'
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_basic
            scope: message.read,message.write
        provider:
          test:
            #todo       provider 尽量用域名  不要用localhost或者IP  而且要和well-known接口中保持一致
            issuer-uri: http://localhost:9000


/.well-known/oauth-authorization-server端点能替代很多provider的配置项,如果同时存在issuer-uri和其它端点,issuer-uri的优先级要低一些,相关的解析逻辑请参考ClientRegistrations类。

1.3、 redirect-uri

/test/bar是一个测试接口:

    /**
     * 测试Spring Authorization Server
     *
     * @see HttpSecurity#oauth2Client()
     * @param client the client
     * @return the map
     */
    @GetMapping("/test/bar")
    public Map<String,Object> bar(@RegisteredOAuth2AuthorizedClient("test") OAuth2AuthorizedClient client){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Map<String, Object> map = new HashMap<>();
        map.put("authentication",authentication);
        // OAuth2AuthorizedClient 为敏感信息不应该返回前端
        map.put("oAuth2AuthorizedClient",client);
        return map;
    }

根据OAuth2ClientConfigurer一文中的讲解,/test/bar需要配置匿名访问,伪代码:


        http.authorizeRequests((requests) -> requests
                        .antMatchers("/test/bar")
                        .hasAnyAuthority("ROLE_ANONYMOUS","SCOPE_userinfo")
                        .anyRequest().authenticated())

其它的复用前面几个客户端HttpSecurity.oauth2Client()的配置即可。

1.4、测试

这里要先启动OAuth2授权服务器,后启动OAuth2客户端,因为客户端需要调用issuer-uri初始化OAuth2授权服务器的信息。

  1. 浏览器打开http://127.0.0.1:8082/test/bar,注意不能用localhost。
  2. 浏览器会跳转授权服务器的登录页面http://localhost:9000/login,依次输入用户名test和密码123456
  3. 然后会重定向到http://localhost:9000/oauth2/authorize,这里增加了一个让用户二次确认的选项,这个功能是我们在授权服务器中特地开启的,你可以勾选同意授权或者直接拒绝授权
  4. 当勾选scope同意授权后,又重定向到/test/bar完成请求拿到了JSON信息,可以看出来确实是一个匿名用户。

在这里插入图片描述

三、授权服务器处理客户端授权请求流程

客户端通过/oauth2/authorize向授权服务器发起了授权请求,这期间发生了什么?通过日志我们来看一个究竟。这里分为两个阶段:

1、用户登录之前

下面是授权服务器收到授权请求处理并跳转到登录/login前的日志:
在这里插入图片描述
日志中标记了四个关键点:

在这里插入图片描述

  1. 授权服务器接收到了授权请求并经过过滤器链处理。
  2. OAuth2AuthorizationEndpointFilter拦截到了授权请求,然后由授权服务器接收到了授权请求并经过过滤器链处理。
  3. HttpSession中对授权请求进行了缓存,然后发现本次授权请求是匿名访问。
  4. 匿名访问被投票拒绝,跳转到登录页。

2、用户登录之后

跳转到登录页后,用户输入用户名和密码登录成功后,会跳转到授权确认页。这里从日志发现重复的还是上面步骤②,说明授权确认页的逻辑还是在OAuth2AuthorizationEndpointFilter中。

3、授权确认后

经过点选确认授权后授权服务器执行了下列逻辑:

  • POST 请求/oauth2/authorize,再次被OAuth2AuthorizationEndpointFilter拦截处理,

  • 由于用户是认证的,通知OAuth2客户端重定向到最开始的redirect_uri地址http://127.0.0.1:8082/test/bar?code=CODE&state=STATE

OAuth2客户端OAuth2AuthorizationCodeGrantFilter拦截到携带了code和state的redirect_uri后向授权服务器发起/oauth2/token请求获取token,具体的流程参考前面的相关文章。

这里需要提及一个重要的知识点,这里由于我们采用的客户端认证方式(ClientAuthenticationMethod)是client_secret_basic,所以获取token的请求是这样的:

POST /oauth2/token HTTP/1.1
Accept: application/json;charset=UTF-8
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Authorization: Basic ZmVsb3JkOnNlY3JldA==
Host: localhost:9000

该请求通过HTTP BASIC的方式来认证客户端。 它使用客户端的client-id和client-secret作为凭证信息,并且使用 BASE64算法进行编码。 而且授权服务器保存得客户端密码是经过摘要的密文。这和前面gitee和wechat采用的机制完全不同,更加安全。那授权服务器如何处理/oauth2/token呢?

其它的ClientAuthenticationMethod方式后面后面也会做一些专门的测试样例。

4、授权服务器发放Token

经过日志分析发现/oauth2/token是被授权服务器的OAuth2TokenEndpointFilter拦截处理的。具体交给了OAuth2AuthorizationCodeAuthenticationProvider来处理,最终返回包含Access TokenOAuth2AccessTokenAuthenticationToken

上面对Spring Authorization Server的案例流程进行了日志分析,发现了几个关键的过滤器。可以预见到Spring Authorization Server的配置就是围绕这几个过滤器展开的。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值