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

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

1. 前文回顾与整合设计

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

整个流程分为两类:

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

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

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

2. gateway实现

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

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

绘制的流程图如下:

2.1 permitAll实现

对外暴露的接口可以直接访问,这可以依赖配置文件,而配置文件又可以通过配置中心进行动态更新,所以不用担心有hard-code的问题。
在配置文件中定义需要permitall的路径。

123456
auth:  permitall:    -      pattern: /login/**    -      pattern: /web/public/**

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

12345
@Bean@ConfigurationProperties(prefix = "auth")public PermitAllUrlProperties getPermitAllUrlProperties() {    return new PermitAllUrlProperties();}

当然还需要有PermitAllUrlProperties对应的实体类,比较简单,不列出来了。

2.2 加强头部

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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
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服务。

123456789101112131415161718192021222324
@Configuration@EnableResourceServerpublic 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的相应依赖。

123456789
<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服务中的相应端点。

123456789
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权限。

123456789
//根据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的通用流程图:

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

4.1 filter过滤器

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

12345678910111213141516171819202122232425262728293031323334
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

1234567
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface PreAuth {    String value();}

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

123456789101112131415161718192021222324252627282930313233
@Component@Aspectpublic 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()转换成标准的操作上下文,然后解析注解中的表达式,最后获取对表达式判断的结果。

123456
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {

    public CustomerSecurityExpressionRoot(Authentication authentication) {        super(authentication);    }}

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

4.3 controller接口

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

12345
@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。

4.5 后期优化

优化的地方主要有两点:

  • 现在的设计是,每次请求过来都会去调用auth服务获取该user相应的权限信息。而后端微服务数量有很多,没必要每个服务,或者说一个服务的多个服务实例,每次都去调用auth服务,笔者认为完全可以引入redis集群的缓存机制,在请求到达一个服务的某个实例时,首先去查询对应的user的缓存中的权限,如果没有再调用auth服务,最后写入redis缓存。当然,如果权限更新了,在auth服务肯定要delete相应的user权限缓存。
  • 关于被拒绝的请求,在切面表达式中,直接返回了对象,笔者认为可以和response status 403进行绑定,定制返回对象的内容,返回的response更加友好。

5. 总结

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

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

可以优化的地方当然还有很多,整合项目中设计不合理的地方,各位同学可以多多提意见。

原文地址:https://www.cnblogs.com/karmapeng/p/12312113.html

时间: 02-15

微服务架构中整合网关、权限服务的相关文章

GoldenGate 12.3微服务架构与传统架构的区别

随着Oracle GoldenGate 12c(12.3.0.1.0)的发布,引入了可用于复制业务数据的新架构. 多年来,这种架构有着不同的称谓,Oracle终于在最后GA发布的版本中,以“Microservices”的名义确认新架构的名称.Microservices架构有很多好处,这些好处应该让您暂停探索Oracle GoldenGate 12c的新功能.在我们进入微服务架构之前,让我们先看一下经典架构.在下图中,您将看到一个非常标准的传统Oracle GoldenGate架构实现. 在这种架

Spring Cloud构建微服务架构(五)服务网关

通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了.比如下图所示: alt 我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现:而服务间通过Ribbon或Feign实现服务的消费以及均衡负载:通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理.为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延. 在该架

微服务架构的基础框架选择:Spring Cloud还是Dubbo?

本文转自:http://mt.sohu.com/20160803/n462486707.shtml 最近一段时间不论互联网还是传统行业,凡是涉及信息技术范畴的圈子几乎都在讨论 微服务架构 .近期也看到各大技术社区开始组织一些沙龙和论坛来分享Spring Cloud的相关实施经验,这对于最近正在整理Spring Cloud相关套件内容与实例应用的我而言,还是有不少激励的. 目前,Spring Cloud在国内的知名度并不高,在前阵子的求职过程中,与一些互联网公司的架构师.技术VP或者CTO在交流时

微服务架构(Microservices)

说在前面 好久没写博文了,心里痒痒(也许是换工作后,有点时间了吧).最近好像谈论微服务的人比较多,也开始学习一下,但是都有E文,看起来半懂不懂的. Martinfowler的<微服务>,也算是入门必读了.有人翻译过,但是只有一半.还是自己练练手吧. 微服务 "微服务架构"一词在过去几年里广泛的传播,它用于描述一种独立部署的软件应用设计方式.这种架构方式并没有非常准确的定义,但是在业务能力.自动部署.端对端的整合.对语言及数据的分散控制上,却有着显著特征. "微服务

微服务架构(Microservice Architecture)

之前一段时间,有听部门架构说起接下来公司要使用微服务架构来研发系统,当时没怎么在意,因为是第一次听说微服务这个名词(果然无知者无畏啊):正好赶上五一假, 我自告奋勇的,接了编写微服务架构培训文档这个任务(也许因为我是文科生,文笔稍微好点).五一假期三天,基本都是在看资料,梳理思路以及编写接下来的培训文档中度过. 下面,就说说我这几天的一些收获吧:先说说资料来源吧:有架构给我的一些资料,以及自己百度和论坛.社区找来的一些资料,权当做一个总结式的简介... 目录如下: 一.微服务架构介绍 二.出现和

MicroService 微服务架构模式简述

原文是 Martin Flower 于 2014 年 3 月 25 日写的<Microservices>. 本文内容 微服务 微服务风格的特性 组件化(Componentization )与服务(Services) 围绕业务功能的组织 产品不是项目 强化终端及弱化通道 分散治理 分散数据管理 基础设施自动化 容错性设计 设计改进 微服务是未来吗 其它 微服务系统多大 微服务与SOA 多语言多选择 实践标准和强制标准 让做对事更容易 断路器circuit breaker和产品中现有的代码 同步是

深解微服务架构:从过去,到未来|架构(2015-07-15)

随着用户需求个性化.产品生命周期变短,微服务架构是未来软件软件架构朝着灵活性.扩展性.伸缩性以及高可用性发展的必然方向.同时,以Docker为代表的容器虚拟化技术的盛行,将大大降低微服务实施的成本,为微服务落地以及大规模使用提供了坚实的基础和保障. 微服务的诞生   微服务架构(Microservice Architect)是一种架构模式,它提倡将单块架构的应用划分成一组小的服务,服务之间互相协调.互相配合,为用户提供最终价值.每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟

基于docker部署的微服务架构(四): 配置中心

原文:http://www.jianshu.com/p/b17d65934b58%20 前言 在微服务架构中,由于服务数量众多,如果使用传统的配置文件管理方式,配置文件分散在各个项目中,不易于集中管理和维护.在 spring cloud 中使用 config-server 集中管理配置文件,可以使用 git.svn.本地资源目录 来管理配置文件,在集成了 spring cloud bus 之后还可以通过一条 post 请求,让所有连接到消息总线的服务,重新从config-server 拉取配置文

面向服务与微服务架构

背景 最近阅读了 Martin Fowler 和 James Lewis 合著的一篇文章 Microservices, 文中主要描述和探讨了最近流行起来的一种服务架构模式--微服务,和我最近几年工作的实践比较相关感觉深受启发.本文吸收了部分原文观点,结合自身实践经验来探讨下服务架构模式的演化. 面向服务架构(SOA) 面向服务架构 SOA 思想概念的提出已不是什么新鲜事,大概在10年前就有不少相关书籍介绍过.当时 java 企业应用领域 J2EE 依然是主流,应用程序被部署在庞大统一的符合 J2

(一)整合spring cloud云服务架构 - Spring Cloud简介

Spring Cloud是一系列框架的有序集合.利用Spring Boot的开发模式简化了分布式系统基础设施的开发,如服务发现.注册.配置中心.消息总线.负载均衡.断路器.数据监控等(这里只简单的列了一部分),都可以用Spring Boot的开发风格做到一键启动和部署.Spring Cloud将目前比较成熟.经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终整合出一套简单易懂.易部署和易维护的分布式系统架构平台. Spring Clou