背景
司内有统一的 OpenApi(annoroad-openapi) 平台对外提供接口服务, 采用的是 OAuth2.0 中的客户端模式,第三方通过我司分配的 clientId/clientSecret 去获取 access token,然后每次请求都要携带 access token,整个认证鉴权是基于 Spring Security + OAuth2.0 来实现的。
之前 OpenApi 业务比较单一(主要针对 CRM ),对接的第三方也比较少(只有 CRM 服务商,以及司内的开发团队),所以,得到 clientId/clientSecret 的第三方被允许使用 OpenApi 中的所有方法也没啥太大的问题。但是,随着业务一点点儿扩展,出现如下 2 种情况:
- 接口不单单只有 CRM 相关的,还包括了其他业务。
- 对接方也不单单只有 CRM 服务商一家了。
基于现有的实现,这样就会有一个问题:
任何一个对接的第三方只要得到了clientId/clientSecret,就有权使用 OpenApi 上的所有接口!!!
为了能够限制对接的第三方所能使用的接口(只允许使用授权了的接口),我们采用了 Spring Security 中的 scope 来实现,流程如下图:
实现
先让我们对 Spring Security + OAuth2.0 主要的「认证服务」和「资源服务」两大块儿有个大概的认识,如下图:
-
资源分类
首先我们需要将资源(接口)进行分类,也就是说定义几个 scope,然后将各个接口归类到到对应的 scope 下,例如:scope url order order/** user user/** -
认证服务
通过 @EnableAuthorizationServer 注解配置,例如:annoroad-oauth-server。
在认证服务中,clientId/clientSecret 的存储我们采用的是数据库,所以,需要将数据库 oauth_client_details 表中的每对儿 clientId/clientSecret 中的 scope 字段设置为对应的值,如下图:
这里的 all、crm、applet、pertest、envtest、lims 与上边提到的 order、user 是一回事儿,每一个都会对应的一类资源,例如:crm 对应 crm/**、 applet 对应 applet/** 等等。 -
资源服务
通过 @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 一致,则表示有访问权限;如果不一致,则表示无访问权限。