通过 Spring Security + OAuth2.0 实现只允许客户端使用被授权的资源

背景

司内有统一的 OpenApi(annoroad-openapi) 平台对外提供接口服务, 采用的是 OAuth2.0 中的客户端模式,第三方通过我司分配的 clientId/clientSecret 去获取 access token,然后每次请求都要携带 access token,整个认证鉴权是基于 Spring Security + OAuth2.0 来实现的。
之前 OpenApi 业务比较单一(主要针对 CRM ),对接的第三方也比较少(只有 CRM 服务商,以及司内的开发团队),所以,得到 clientId/clientSecret 的第三方被允许使用 OpenApi 中的所有方法也没啥太大的问题。但是,随着业务一点点儿扩展,出现如下 2 种情况:

  1. 接口不单单只有 CRM 相关的,还包括了其他业务。
  2. 对接方也不单单只有 CRM 服务商一家了。

基于现有的实现,这样就会有一个问题:

任何一个对接的第三方只要得到了clientId/clientSecret,就有权使用 OpenApi 上的所有接口!!!

为了能够限制对接的第三方所能使用的接口(只允许使用授权了的接口),我们采用了 Spring Security 中的 scope 来实现,流程如下图:
通过 Spring Security + OAuth2.0 实现只允许客户端使用被授权的资源_第1张图片

实现

先让我们对 Spring Security + OAuth2.0 主要的「认证服务」和「资源服务」两大块儿有个大概的认识,如下图:
通过 Spring Security + OAuth2.0 实现只允许客户端使用被授权的资源_第2张图片

  1. 资源分类
    首先我们需要将资源(接口)进行分类,也就是说定义几个 scope,然后将各个接口归类到到对应的 scope 下,例如:

    scope url
    order order/**
    user user/**
  2. 认证服务

    通过 @EnableAuthorizationServer 注解配置,例如:annoroad-oauth-server。

    在认证服务中,clientId/clientSecret 的存储我们采用的是数据库,所以,需要将数据库 oauth_client_details 表中的每对儿 clientId/clientSecret 中的 scope 字段设置为对应的值,如下图:
    在这里插入图片描述 这里的 all、crm、applet、pertest、envtest、lims 与上边提到的 order、user 是一回事儿,每一个都会对应的一类资源,例如:crm 对应 crm/**、 applet 对应 applet/** 等等。

  3. 资源服务

    通过 @EnableResourceServer 注解配置,例如:annoroad-openapi

    需要在资源服务中指定资源适用的 scope,代码如下:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
    
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Autowired
        RedisConnectionFactory redisConnectionFactory;
    
        private static final String DEMO_RESOURCE_ID = "all"; // 所有对接方都被授权使用的接口
        private static final String CRM_RESOURCE_ID = "crm"; // CRM相关接口
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources
                    /**
                     * 1、stateless==fasle 为关闭状态,则 access token 使用时的 session id 会被记录,后续请求不携带 access token 也可以正常响应
                     * 2、stateless==true 为打开状态,则每次请求都必须携带 access token 请求才行,否则将无法访问
                     */
                    .resourceId(DEMO_RESOURCE_ID).stateless(true)
                    .resourceId(CRM_RESOURCE_ID).stateless(true)
                    .tokenStore(new RedisTokenStore(redisConnectionFactory)).stateless(true);
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置 Spring Security 永远不会创建 HttpSession(不会使用 HttpSession 来获取 SecurityContext)
                    .and()
                    .authorizeRequests().antMatchers("/demo/**").authenticated()
                    .antMatchers("/demo/**").access("#oauth2.hasScope('" + DEMO_RESOURCE_ID + "')")
                    .and()
                    .authorizeRequests().antMatchers("/contract/**").authenticated()
                    .antMatchers("/contract/**").access("#oauth2.hasScope('" + CRM_RESOURCE_ID + "')")
                    .and()
                    .anyRequest().anonymous();
        }
    }
    

    资源请求所携带的 access token 会有一个对应的 scope(该 scope 与 access token 对应的 clientId/clientSecret 在数据库中的 scope 一致),该 scope 如果与被请求资源的 scope 一致,则表示有访问权限;如果不一致,则表示无访问权限。

你可能感兴趣的:(Spring)