148

认证鉴权与API权限控制在微服务架构中的设计与实现(三)

 6 years ago
source link: http://mp.weixin.qq.com/s/nIHEQ2KbRF-6d9YGjJZhMA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引言: 本文系《认证鉴权与API权限控制在微服务架构中的设计与实现》系列的第三篇,本文重点讲解token以及API级别的鉴权。本文对涉及到的大部分代码进行了分析,欢迎订阅本系列文章。

1. 前文回顾

在开始讲解这一篇文章之前,先对之前两篇文章进行回忆下。在第一篇 《认证鉴权与API权限控制在微服务架构中的设计与实现(一)》介绍了该项目的背景以及技术调研与最后选型。第二篇《认证鉴权与API权限控制在微服务架构中的设计与实现(二)》画出了简要的登录和校验的流程图,并重点讲解了用户身份的认证与token发放的具体实现。

Image

本文重点讲解鉴权,包括两个方面:token合法性以及API级别的操作权限。首先token合法性很容易理解,第二篇文章讲解了获取授权token的一系列流程,token是否是认证服务器颁发的,必然是需要验证的。其次对于API级别的操作权限,将上下文信息不具备操作权限的请求直接拒绝,当然此处是设计token合法性校验在先,其次再对操作权限进行验证,如果前一个验证直接拒绝,通过则进入操作权限验证。

2.资源服务器配置

ResourceServer配置在第一篇就列出了,在进入鉴权之前,把这边的配置搞清,即使有些配置在本项目中没有用到,大家在自己的项目有可能用到。



  1. @Configuration

  2. @EnableResourceServer

  3. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

  4.    //http安全配置

  5.    @Override

  6.    public void configure(HttpSecurity http) throws Exception {

  7.        //禁掉csrf,设置session策略

  8.        http.csrf().disable()

  9.                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

  10.                .and()//默认允许访问

  11.                .requestMatchers().antMatchers("/**")

  12.                .and().authorizeRequests()

  13.                .antMatchers("/**").permitAll()

  14.                .anyRequest().authenticated()

  15.                .and().logout() //logout注销端点配置

  16.                .logoutUrl("/logout")

  17.                .clearAuthentication(true)

  18.                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())

  19.                .addLogoutHandler(customLogoutHandler());

  20.    }

  21.    //添加自定义的CustomLogoutHandler

  22.    @Bean

  23.    public CustomLogoutHandler customLogoutHandler() {

  24.        return new CustomLogoutHandler();

  25.    }

  26.    //资源安全配置相关

  27.    @Override

  28.    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

  29.        super.configure(resources);

  30.    }

  31. }

(1). @EnableResourceServer这个注解很重要,OAuth2资源服务器的简便注解。其使得Spring Security filter通过请求中的OAuth2 token来验证请求。通常与 EnableWebSecurity配合使用,该注解还创建了硬编码的 @Order(3)WebSecurityConfigurerAdapter,由于当前spring的技术,order的顺序不易修改,所以在项目中避免还有其他order=3的配置。

(2). 关联的 HttpSecurity,与之前的 Spring Security XML中的 "http"元素配置类似,它允许配置基于web安全以针对特定http请求。默认是应用到所有的请求,通过 requestMatcher可以限定具体URL范围。HttpSecurity类图如下。

Image

总的来说:HttpSecurity是SecurityBuilder接口的一个实现类,从名字上我们就可以看出这是一个HTTP安全相关的构建器。当然我们在构建的时候可能需要一些配置,当我们调用HttpSecurity对象的方法时,实际上就是在进行配置。

authorizeRequests(),formLogin()、httpBasic()这三个方法返回的分别是 ExpressionUrlAuthorizationConfigurer、 FormLoginConfigurer、 HttpBasicConfigurer,他们都是SecurityConfigurer接口的实现类,分别代表的是不同类型的安全配置器。
因此,从总的流程上来说,当我们在进行配置的时候,需要一个安全构建器SecurityBuilder(例如我们这里的HttpSecurity),SecurityBuilder实例的创建需要有若干安全配置器SecurityConfigurer实例的配合。

(3).关联的 ResourceServerSecurityConfigurer,为资源服务器添加特殊的配置,默认的适用于很多应用,但是这边的修改至少以resourceId为单位。类图如下。

Image

ResourceServerSecurityConfigurer创建了OAuth2核心过滤器 OAuth2AuthenticationProcessingFilter,并为其提供固定了 OAuth2AuthenticationManager。只有被 OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。同时设置了TokenExtractor、异常处理实现。

OAuth2AuthenticationProcessingFilter是OAuth2保护资源的预先认证过滤器。配合 OAuth2AuthenticationManager使用,根据请求获取到OAuth2 token,之后就会使用 OAuth2Authentication来填充Spring Security上下文。
OAuth2AuthenticationManager在前面的文章给出的 AuthenticationManager类图就出现了,与token认证相关。这边略过贴出源码进行讲解,读者可以自行阅读。

3. 鉴权endpoint

鉴权主要是使用内置的endpoint /oauth/check_token,笔者将对端点的分析放在前面,因为这是鉴权的唯一入口。下面我们来看下该API接口中的主要代码。



  1.    @RequestMapping(value = "/oauth/check_token")

  2.    @ResponseBody

  3.    public Map<String, ?> checkToken(CheckTokenEntity checkTokenEntity) {

  4.        //CheckTokenEntity为自定义的dto

  5.        Assert.notNull(checkTokenEntity, "invalid token entity!");

  6.        //识别token

  7.        OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(checkTokenEntity.getToken());

  8.        //判断token是否为空

  9.        if (token == null) {

  10.            throw new InvalidTokenException("Token was not recognised");

  11.        }

  12.        //未过期

  13.        if (token.isExpired()) {

  14.            throw new InvalidTokenException("Token has expired");

  15.        }

  16.        //加载OAuth2Authentication

  17.        OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

  18.        //获取response,token合法性验证完毕

  19.        Map<String, Object> response = (Map<String, Object>) accessTokenConverter.convertAccessToken(token, authentication);

  20.        //check for api permission

  21.        if (response.containsKey("jti")) {

  22.            //上下文操作权限校验

  23.            Assert.isTrue(checkPermissions.checkPermission(checkTokenEntity));

  24.        }

  25.        response.put("active", true);    // Always true if token exists and not expired

  26.        return response;

  27.    }

看过security-oauth源码的同学可能立马就看出上述代码与源码不同,或者熟悉 /oauth/check_token校验流程的也会看出来,这边笔者对 security-oauth jar进行了重新编译,修改了部分源码用于该项目需求的场景。主要是加入了前置的API级别的权限校验。

4. token 合法性验证

从上面的 CheckTokenEndpoint中可以看出,对于token合法性验证首先是识别请求体中的token。用到的主要方法是 ResourceServerTokenServices提供的 readAccessToken()方法。该接口的实现类为 DefaultTokenServices,在之前的配置中有讲过这边配置了jdbc的TokenStore。



  1. public class JdbcTokenStore implements TokenStore {

  2.    ...

  3.    public OAuth2AccessToken readAccessToken(String tokenValue) {

  4.        OAuth2AccessToken accessToken = null;

  5.        try {

  6.            //使用selectAccessTokenSql语句,调用了私有的extractTokenKey()方法

  7.            accessToken = jdbcTemplate.queryForObject(selectAccessTokenSql, new RowMapper<OAuth2AccessToken>() {

  8.                public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {

  9.                    return deserializeAccessToken(rs.getBytes(2));

  10.                }

  11.            }, extractTokenKey(tokenValue));

  12.        }

  13.        //异常情况

  14.        catch (EmptyResultDataAccessException e) {

  15.            if (LOG.isInfoEnabled()) {

  16.                LOG.info("Failed to find access token for token " + tokenValue);

  17.            }

  18.        }

  19.        catch (IllegalArgumentException e) {

  20.            LOG.warn("Failed to deserialize access token for " + tokenValue, e);

  21.            //不合法则移除

  22.            removeAccessToken(tokenValue);

  23.        }

  24.        return accessToken;

  25.    }

  26.    ...

  27.    //提取TokenKey方法

  28.    protected String extractTokenKey(String value) {

  29.        if (value == null) {

  30.            return null;

  31.        }

  32.        MessageDigest digest;

  33.        try {

  34.            //MD5

  35.            digest = MessageDigest.getInstance("MD5");

  36.        }

  37.        catch (NoSuchAlgorithmException e) {

  38.            throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).");

  39.        }

  40.        try {

  41.            byte[] bytes = digest.digest(value.getBytes("UTF-8"));

  42.            return String.format("%032x", new BigInteger(1, bytes));

  43.        }

  44.        catch (UnsupportedEncodingException e) {

  45.            throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).");

  46.        }

  47.    }

  48. }

readAccessToken()检索出该token值的完整信息。上述代码比较简单,涉及到的逻辑也不复杂,此处简单讲解。下图为debug token校验的变量信息,读者可以自己动手操作下,截图仅供参考。

Image

至于后面的步骤, loadAuthentication()为特定的access token 加载credentials。得到的credentials 与token作为 convertAccessToken()参数,得到校验token的response。

5. API级别权限校验

笔者项目目前都是基于Web的权限验证,之前遗留的一个巨大的单体应用系统正在逐渐拆分,然而当前又不能完全拆分完善。为了同时兼容新旧服务,尽量减少对业务系统的入侵,实现微服务的统一性和独立性。笔者根据业务业务场景,尝试在Auth处做操作权限校验。 首先想到的是资源服务器配置ResourceServer,如:



  1. http.authorizeRequests()

  2. .antMatchers("/order/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")

这样做需要将每个操作接口的API权限控制放在各个不同的业务服务,每个服务在接收到请求后,需要先从Auth服务取出该token 对应的role和scope等权限信息。这个方法肯定是可行的,但是由于项目鉴权的粒度更细,而且暂时不想大动原有系统,在加上之前网关设计,网关调用Auth服务校验token合法性,所以最后决定在Auth系统调用中,把这些校验一起解决完。

文章开头资源服务器的配置代码可以看出,对于所有的资源并没有做拦截,因为网关处是调用Auth系统的相关endpoint,并不是所有的请求url都会经过一遍Auth系统,所以对于所有的资源,在Auth系统中,定义需要鉴权接口所需要的API权限,然后根据上下文进行匹配。这是采用的第二种方式,也是笔者目前采用的方法。当然这种方式的弊端也很明显,一旦并发量大,网关还要耗时在调用Auth系统的鉴权上,TPS势必要下降很多,对于一些不需要鉴权的服务接口也会引起不可用。另外一点是,对于某些特殊权限的接口,需要的上下文信息很多,可能并不能完全覆盖,对于此,笔者的解决是分两方面:一是尽量将这些特殊情况进行分类,某一类的情况统一解决;二是将严苛的校验降低,对于上下文校验失败的直接拒绝,而通过的,对于某些接口,在接口内进行操作之前,对特殊的地方还要再次进行校验。

上面在讲endpoint有提到这边对源码进行了改写。 CheckTokenEntity是自定义的DTO,这这个类中定义了鉴权需要的上下文,这里是指能校验操作权限的最小集合,如URI、roleId、affairId等等。另外定义了 CheckPermissions接口,其方法 checkPermission(CheckTokenEntitycheckTokenEntity)返回了check的结果。而其具体实现类则定义在Auth系统中。笔者项目中调用的实例如下:



  1. @Component

  2. public class CustomCheckPermission implements CheckPermissions {

  3.    @Autowired

  4.    private PermissionService permissionService;

  5.    @Override

  6.    public boolean checkPermission(CheckTokenEntity checkTokenEntity) {

  7.        String url = checkTokenEntity.getUri();

  8.        Long affairId = checkTokenEntity.getAffairId();

  9.        Long roleId = checkTokenEntity.getRoleId();

  10.        //校验

  11.        if (StringUtils.isEmpty(url) || affairId <= 0 || roleId <= 0) {

  12.            return true;

  13.        } else {

  14.            return permissionService.checkPermission(url, affairId, roleId);

  15.        }

  16.    }

  17. }

关于jar包 spring-cloud-starter-oauth2中的具体修改内容,大家可以看下文末笔者的GitHub项目。通过自定义 CustomCheckPermission,覆写 checkPermission()方法,大家也可以对自己业务的操作权限进行校验,非常灵活。这边涉及到具体业务,笔者在项目中只提供接口,具体的实现需要读者自行完成。

本文相对来说比较简单,主要讲解了token以及API级别的鉴权。token的合法性认证很常规,Auth系统对于API级别的鉴权是结合自身业务需要和现状进行的设计。这两块的校验都前置到Auth系统中,优缺点在上面的小节也有讲述。最后,架构设计根据自己的需求和现状,笔者的解决思路仅供参考。

本文的源码地址:
GitHub:https://github.com/keets2012/Auth-service
码云: https://gitee.com/keets/Auth-Service

订阅最新文章,欢迎关注我的公众号

Image

  1. 微服务API级权限的技术架构

  2. spring-security-oauth

  3. Spring-Security Docs

认证鉴权与API权限控制在微服务架构中的设计与实现(一)
认证鉴权与API权限控制在微服务架构中的设计与实现(二)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK