微服务架构中整合网关、权限服务

前言:以前的文章有讲过微服务的权限系列和网关实现,都是孤立存在,本文将整合后端服务与网关、权限系统。安全权限部分的实现还讲解了基于前置验证的方式实现,可是因为与业务联系比较紧密,没有具体的示例。业务权限与业务联系很是密切,本次的整合项目将会把这部分的操做权限校验实现基于具体的业务服务。java

1. 前文回顾与整合设计

认证鉴权与API权限控制在微服务架构中的设计与实现系列文章中,讲解了在微服务架构中Auth系统的受权认证和鉴权。在微服务网关中,讲解了基于netflix-zuul组件实现的微服务网关。下面咱们看一下此次整合的架构图。git

ms
微服务架构权限

整个流程分为两类:github

  • 用户还没有登陆。客户端(web和移动端)发起登陆请求,网关对于登陆请求直接转发到auth服务,auth服务对用户身份信息进行校验(整合项目省略用户系统,读者可自行实现,直接硬编码返回用户信息),最终将身份合法的token返回给客户端。
  • 用户已登陆,请求其余服务。这种状况,客户端的请求到达网关,网关会调用auth系统进行请求身份合法性的验证,验证不通则直接拒绝,并返回401;若是经过验证,则转发到具体服务,服务通过过滤器,根据请求头部中的userId,获取该user的安全权限信息。利用切面,对该接口须要的权限进行校验,经过则proceed,不然返回403。

第一类其实比较简单,在讲解认证鉴权与API权限控制在微服务架构中的设计与实现就基本实现,如今要作的是与网关进行结合;第二类中,咱们新建了一个后端服务,与网关、auth系统整合。web

下面对整合项目涉及到的三个服务分别介绍。网关和auth服务的实现已经讲过,本文主要讲下这两个服务进行整合须要的改动,还有就是对于后端服务的主要实现进行讲解。spring

2. gateway实现

微服务网关已经基本介绍完了网关的实现,包括服务路由、几种过滤方式等。这一节将重点介绍实际应用时的整合。对于须要修改加强的地方以下:sql

  • 区分暴露接口(即对外直接访问)和须要合法身份登陆以后才能访问的接口
  • 暴露接口直接放行,转发到具体服务,如登陆、刷新token等
  • 须要合法身份登陆以后才能访问的接口,根据传入的Access token进行构造头部,头部主要包括userId等信息,可根据本身的实际业务在auth服务中进行设置。
  • 最后,比较重要的一点,引入Spring Security的资源服务器配置,对于暴露接口设置permitAll(),其他接口进入身份合法性校验的流程,调用auth服务,若是经过则正常继续转发,不然抛出异常,返回401。

绘制的流程图以下:express

gwflow
网关路由流程图

2.1 permitAll实现

对外暴露的接口能够直接访问,这能够依赖配置文件,而配置文件又能够经过配置中心进行动态更新,因此不用担忧有hard-code的问题。 在配置文件中定义须要permitall的路径。后端

auth:
 permitall:
 -
 pattern: /login/**
 -
 pattern: /web/public/**
复制代码

服务启动时,读入相应的Configuration,下面的配置属性读取以auth开头的配置。api

@Bean
    @ConfigurationProperties(prefix = "auth")
    public PermitAllUrlProperties getPermitAllUrlProperties() {
        return new PermitAllUrlProperties();
    }
复制代码

固然还须要有PermitAllUrlProperties对应的实体类,比较简单,不列出来了。安全

2.2 增强头部

Filter过滤器,它是Servlet技术中最实用的技术,Web开发人员经过Filter技术,对web服务器管理的全部web资源进行拦截。这边使用Filter进行头部加强,解析请求中的token,构造统一的头部信息,到了具体服务,能够利用头部中的userId进行操做权限获取与判断。

public class HeaderEnhanceFilter implements Filter {

	//...

    @Autowired
    private PermitAllUrlProperties permitAllUrlProperties;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

	//主要的过滤方法
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String authorization = ((HttpServletRequest) servletRequest).getHeader("Authorization");
        String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
        // test if request url is permit all , then remove authorization from header
        LOGGER.info(String.format("Enhance request URI : %s.", requestURI));
        //将isPermitAllUrl的请求进行传递
        if(isPermitAllUrl(requestURI) && isNotOAuthEndpoint(requestURI)) {
        	//移除头部,但不包括登陆端点的头部
            HttpServletRequest resetRequest = removeValueFromRequestHeader((HttpServletRequest) servletRequest);
            filterChain.doFilter(resetRequest, servletResponse);
            return;
        }
        //判断是否是符合规范的头部
        if (StringUtils.isNotEmpty(authorization)) {
            if (isJwtBearerToken(authorization)) {
                try {
                    authorization = StringUtils.substringBetween(authorization, ".");
                    String decoded = new String(Base64.decodeBase64(authorization));

                    Map properties = new ObjectMapper().readValue(decoded, Map.class);
					//解析authorization中的token,构造USER_ID_IN_HEADER
                    String userId = (String) properties.get(SecurityConstants.USER_ID_IN_HEADER);

                    RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, userId);
                } catch (Exception e) {
                    LOGGER.error("Failed to customize header for the request", e);
                }
            }
        } else {
          //为了适配,设置匿名头部
            RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, ANONYMOUS_USER_ID);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
    
    //...
    
}
复制代码

上面代码列出了头部加强的基本处理流程,将isPermitAllUrl的请求进行直接传递,不然判断是否是符合规范的头部,而后解析authorization中的token,构造USER_ID_IN_HEADER。最后为了适配,设置匿名头部。
须要注意的是,HeaderEnhanceFilter也要进行注册。Spring 提供了FilterRegistrationBean类,此类提供setOrder方法,能够为filter设置排序值,让spring在注册web filter以前排序后再依次注册。

2.3 资源服务器配置

利用资源服务器的配置,控制哪些是暴露端点不须要进行身份合法性的校验,直接路由转发,哪些是须要进行身份loadAuthentication,调用auth服务。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	//... 
	//配置permitAll的请求pattern,依赖于permitAllUrlProperties对象
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .requestMatchers().antMatchers("/**")
                .and()
                .authorizeRequests()
                .antMatchers(permitAllUrlProperties.getPermitallPatterns()).permitAll()
                .anyRequest().authenticated();
    }

	//经过自定义的CustomRemoteTokenServices,植入身份合法性的相关验证
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        CustomRemoteTokenServices resourceServerTokenServices = new CustomRemoteTokenServices();
        //...
        resources.tokenServices(resourceServerTokenServices);
    }
}
复制代码

资源服务器的配置你们看了笔者以前的文章应该很熟悉,此处不过多重复讲了。关于ResourceServerSecurityConfigurer配置类,以前的安全系列文章已经讲过,ResourceServerTokenServices接口,当时咱们也用到了,只不过用的是默认的DefaultTokenServices。这边经过自定义的CustomRemoteTokenServices,植入身份合法性的相关验证。

固然这个配置还要引入Spring Cloud Security oauth2的相应依赖。

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
复制代码

2.4 自定义RemoteTokenServices实现

ResourceServerTokenServices接口其中的一个实现是RemoteTokenServices

Queries the /check_token endpoint to obtain the contents of an access token. If the endpoint returns a 400 response, this indicates that the token is invalid.

RemoteTokenServices主要是查询auth服务的/check_token端点以获取一个token的校验结果。若是有错误,则说明token是不合法的。笔者这边的的CustomRemoteTokenServices实现就是沿用该思路。须要注意的是,笔者的项目基于Spring cloud,auth服务是多实例的,因此这边使用了Netflix Ribbon获取auth服务进行负载均衡。Spring Cloud Security添加以下默认配置,对应auth服务中的相应端点。

security:
 oauth2:
 client:
 accessTokenUri: /oauth/token
 clientId: gateway
 clientSecret: gateway
 resource:
 userInfoUri: /user
 token-info-uri: /oauth/check_token
复制代码

至于具体的CustomRemoteTokenServices实现,能够参考上面讲的思路以及RemoteTokenServices,很简单,此处略去。

至此,网关服务的加强完成,下面看一下咱们对auth服务和后端backend服务的实现。
强调一下,为何头部传递的userId等信息须要在网关构造?读者能够本身思考一下,结合安全等方面,😆笔者暂时不给出答案。

3. auth整合

auth服务的整合修改,其实没那么多,以前对于user、role以及permission之间的定义和关系没有给出实现,这部分的sql语句已经在auth.sql中。因此为了能给出一个完整的实例,笔者把这部分实现给补充了,主要就是user-role,role、role-permission的相应接口定义与实现,实现增删改查。

读者要是想参考整合项目进行实际应用,这部分彻底能够根据本身的业务进行加强,包括token的建立,其自定义的信息还能够在网关中进行统一处理,构造好以后传递给后端服务。

这边的接口只是列出了须要的几个,其余接口没写(由于懒。。)

这两个接口也是给backend项目用来获取相应的userId权限。

//根据userId获取用户对应的权限
 @RequestMapping(method = RequestMethod.GET, value = "/api/userPermissions?userId={userId}",
            consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    List<Permission> getUserPermissions(@RequestParam("userId") String userId);

//根据userId获取用户对应的accessLevel(好像暂时没用到。。)
    @RequestMapping(method = RequestMethod.GET, value = "/api/userAccesses?userId={userId}",
            consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    List<UserAccess> getUserAccessList(@RequestParam("userId") String userId);
复制代码

好了,这边的实现已经讲完了,具体见项目中的实现。

4. backend项目实现

本节是进行实现一个backend的实例,后端项目主要实现哪些功能呢?咱们考虑一下,以前网关服务和auth服务所作的准备:

  • 网关构造的头部userId(可能还有其余信息,这边只是示例),能够在backend得到
  • 转发到backend服务的请求,都是通过身份合法性校验,或者是直接对外暴露的接口
  • auth服务,提供根据userId进行获取相应的权限的接口

根据这些,笔者绘制了一个backend的通用流程图:

bf
backend流程图

上面的流程图其实已经很是清晰了,首先通过filter过滤器,填充SecurityContextHolder的上下文。其次,经过切面来实现注解,是否须要进入切面表达式处理。不须要的话,直接执行接口内的方法;不然解析注解中须要的权限,判断是否有权限执行,有的话继续执行,不然返回403 forbidden。

4.1 filter过滤器

Filter过滤器,和上面网关使用同样,拦截客户的HttpServletRequest。

public class AuthorizationFilter implements Filter {

    @Autowired
    private FeignAuthClient feignAuthClient;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        logger.info("过滤器正在执行...");
        // pass the request along the filter chain
        String userId = ((HttpServletRequest) servletRequest).getHeader(SecurityConstants.USER_ID_IN_HEADER);

        if (StringUtils.isNotEmpty(userId)) {
            UserContext userContext = new UserContext(UUID.fromString(userId));
            userContext.setAccessType(AccessType.ACCESS_TYPE_NORMAL);

            List<Permission> permissionList = feignAuthClient.getUserPermissions(userId);
            List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
            for (Permission permission : permissionList) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority();
                authority.setAuthority(permission.getPermission());
                authorityList.add(authority);
            }

            CustomAuthentication userAuth  = new CustomAuthentication();
            userAuth.setAuthorities(authorityList);
            userContext.setAuthorities(authorityList);
            userContext.setAuthentication(userAuth);
            SecurityContextHolder.setContext(userContext);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    
	//...
}
复制代码

上述代码主要实现了,根据请求头中的userId,利用feign client获取auth服务中的该user所具备的权限集合。以后构造了一个UserContext,UserContext是自定义的,实现了Spring Security的UserDetails, SecurityContext接口。

4.2 经过切面来实现@PreAuth注解

基于Spring的项目,使用Spring的AOP切面实现注解是比较方便的一件事,这边咱们使用了自定义的注解@PreAuth

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
    String value();
}
复制代码

Target用于描述注解的使用范围,超出范围时编译失败,能够用在方法或者类上面。在运行时生效。不了解注解相关知识的,能够自行Google。

@Component
@Aspect
public class AuthAspect {


    @Pointcut("@annotation(com.blueskykong.auth.demo.annotation.PreAuth)")
    private void cut() {
    }

    /** * 定制一个环绕通知,当想得到注解里面的属性,能够直接注入该注解 * * @param joinPoint * @param preAuth */
    @Around("cut()&&@annotation(preAuth)")
    public Object record(ProceedingJoinPoint joinPoint, PreAuth preAuth) throws Throwable {
		//取出注解中的表达式
        String value = preAuth.value();
        //Spring EL 对value进行解析
        SecurityExpressionOperations operations = new CustomerSecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication());
        StandardEvaluationContext operationContext = new StandardEvaluationContext(operations);
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(value);
        //获取表达式判断的结果
        boolean result = expression.getValue(operationContext, boolean.class);
        if (result) {
        	//继续执行接口内的方法
            return joinPoint.proceed();
        }
        return "Forbidden";
    }
}
复制代码

由于Aspect做用在bean上,因此先用Component把这个类添加到容器中。@Pointcut定义要拦截的注解。@Around定制一个环绕通知,当想得到注解里面的属性,能够直接注入该注解。切面表达式内主要实现了,利用Spring EL对value进行解析,将SecurityContextHolder.getContext()转换成标准的操做上下文,而后解析注解中的表达式,最后获取对表达式判断的结果。

public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {

    public CustomerSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }
}
复制代码

CustomerSecurityExpressionRoot继承的是抽象类SecurityExpressionRoot,而咱们用到的实际表达式是定义在SecurityExpressionOperations接口,SecurityExpressionRoot又实现了SecurityExpressionOperations接口。不过这里面的具体判断实现,Spring Security 调用的也是Spring EL。

4.3 controller接口

下面咱们看看最终接口是怎么用上面实现的注解。

@RequestMapping(value = "/test", method = RequestMethod.GET)
    @PreAuth("hasAuthority('CREATE_COMPANY')") // 还能够定义不少表达式,如hasRole('Admin')
    public String test() {
        return "ok";
    }
复制代码

@PreAuth中,能够定义的表达式不少,能够看SecurityExpressionOperations接口中的方法。目前笔者只是实现了hasAuthority()表达式,若是你想支持其余全部表达式,只须要构造相应的SecurityContextHolder便可。

4.4 为何这样设计?

有些读者看了上面的设计,既然好多用到了Spring Security的工具类,确定会问,为何要引入这么复杂的工具类?

其实很简单,首先由于SecurityExpressionOperations接口中定义的表达式足够多,且较为合理,可以覆盖咱们在平时用到的大部分场景;其次,笔者以前的设计是直接在注解中指定所需权限,没有扩展性,且可读性查;最后,Spring Security 4 确实引入了@PreAuthorize,@PostAuthorize等注解,原本想用来着,本身尝试了一下,发现对于微服务架构这样的接口级别的操做权限校验不是很适合,十多个过滤器太过复杂,并且还涉及到的Principal、Credentials等信息,这些已经在auth系统实现了身份合法性校验。笔者认为这边的功能实现并非很复杂,须要很轻量的实现,读者有兴趣能够试着这部分的实现封装成jar包或者Spring Boot的starter。

5. 总结

如上,首先讲了整合的设计思路,主要包含三个服务:gateway、auth和backend demo。整合的项目,整体比较复杂,其中gateway服务扩充了好多内容,对于暴露的接口进行路由转发,这边引入了Spring Security 的starter,配置资源服务器对暴露的路径进行放行;对于其余接口须要调用auth服务进行身份合法性校验,保证到达backend的请求都是合法的或者公开的接口;auth服务在以前的基础上,补充了role、permission、user相应的接口,供外部调用;backend demo是新起的服务,实现了接口级别的操做权限的校验,主要用到了自定义注解和Spring AOP切面。

因为实现的细节实在有点多,本文限于篇幅,只对部分重要的实现进行列出与讲解。若是读者有兴趣实际的应用,能够根据实际的业务进行扩增一些信息,如auth受权的token、网关拦截请求构造的头部信息、注解支持的表达式等等。

能够优化的地方固然还有不少,整合项目中设计不合理的地方,各位同窗能够多多提意见。

推荐阅读

  1. 微服务网关netflix-zuul
  2. 认证鉴权与API权限控制在微服务架构中的设计与实现(一)
  3. 认证鉴权与API权限控制在微服务架构中的设计与实现(二)
  4. 认证鉴权与API权限控制在微服务架构中的设计与实现(三)
  5. 认证鉴权与API权限控制在微服务架构中的设计与实现(四)

源码

网关、auth权限服务和backend服务的整合项目地址为:
GitHub:https://github.com/keets2012/microservice-integration
或者 码云:https://gitee.com/keets/microservice-integration

订阅最新文章,欢迎关注个人公众号

微信公众号
相关文章
相关标签/搜索