oauth2 with spring cloud security (转载)

因项目需要,需要和三方的oauth2服务器进行集成。网上关于spring cloud security oauth2的相关资料,一般都是讲如何配置,而能把这块原理讲透彻的比较少,这边自己做一下总结和整理,顺带介绍一下JWT的使用场景。

什么是OAuth2?

OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式,简单模式,密码模式和客户端模式,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的理解OAuth 2.0,里面有非常详细的说明。

这里要先明确几个OAuth2中的几个重要概念:

  • resource owner: 拥有被访问资源的用户

  • user-agent: 一般来说就是浏览器

  • client: 第三方应用

Authorization server: 认证服务器,用来进行用户认证并颁发token

Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问

明确概念后,就可以看OAuth2的协议握手流程,摘自RFC6749



Abstract Protocol Flow.png

什么是Spring Security?

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过custom-filter来自定义扩展filter chain列表

Alias

Filter Class

Namespace Element or Attribute

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilter

N/A

CAS_FILTER

CasAuthenticationFilter

N/A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N/A

这里面最核心的就是FILTER_SECURITY_INTERCEPTOR,通过FilterInvocationSecurityMetadataSource来进行资源权限的匹配,AccessDecisionManager来执行访问策略。

认证与授权(Authentication and Authorization)

一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有CAS、SAML2、OAUTH2等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。

Spring Cloud Security Oauth2认证流程

将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。

为了便于理解,现在假设有一个名叫“脸盆网”的社交网站,用户在首次登陆时会要求导入用户在facebook的好友列表,以便于快速建立社交关系。具体的授权流程如下:

用户登陆脸盆网,脸盆网试图访问facebook上的好友列表

脸盆网发现该资源是facebook的受保护资源,于是返回302将用户重定向至facebook登陆页面

用户完成认证后,facebook提示用户是否将好友列表资源授权给脸盆网使用(如果本来就是已登陆facebook状态则直接显示是否授权的页面)

用户确认后,脸盆网通过授权码模式获取了facebook颁发的access_token

脸盆网携带该token访问facebook的获取用户接口https://api.facebook.com/user,facebook验证token无误后返回了与该token绑定的用户信息

脸盆网的spring security安全框架根据返回的用户信息构造出了principal对象并保存在session中

脸盆网再次携带该token访问好友列表,facebook根据该token对应的用户返回该用户的好友列表信息

该用户后续在脸盆网发起的访问facebook上的资源,只要在token有效期及权限范围内均可以正常获取(比如想访问一下保存在facebook里的相册)

不难看出,这个假设的场景中,脸盆网就是第三方应用(client),而facebook既充当了认证服务器,又充当了资源服务器。这个流程里面有几个比较重要的关键点,我需要重点说一下,而这也是其他的涉及spring security与OAuth2整合的文章中很少提及的,很容易云里雾里的地方。

细心的同学应该发现了,其实在标准的OAuth2授权过程中,5、6、8这几步都不是必须的,从上面贴的RFC6749规范来看,只要有1、2、3、4、7这几步,就完成了被保护资源访问的整个过程。事实上,RFC6749协议规范本身也并不关心用户身份的部分,它只关心token如何颁发,如何续签,如何用token访问被保护资源(facebook只要保证返回给脸盆网的就是当前用户的好友,至于当前用户是谁脸盆网不需要关心)。那为什么spring security还要做5、6这两步呢?这是因为spring security是一套完整的安全框架,它必须关心用户身份!在实际的使用场景中,OAuth2一般不仅仅用来进行被保护资源的访问,还会被用来做单点登陆(SSO)。在SSO的场景中,用户身份无疑就是核心,而token本身是不携带用户信息的,这样client就没法知道认证服务器发的token到底对应的是哪个用户。设想一下这个场景,脸盆网不想自建用户体系了,想直接用facebook的用户体系,facebook的用户和脸盆网的用户一一对应(其实在很多中小网站现在都是这种模式,可以选择使用微信、QQ、微博等网站的用户直接登陆),这种情况下,脸盆网在通过OAuth2的认证后,就希望拿到用户信息了。所以现在一般主流的OAuth2认证实现,都会预留一个用户信息获取接口,就是上面提到的https://api.facebook.com/user(虽然这不是OAuth2授权流程中必须的),这样client在拿到token后,就可以携带token通过这个接口获取用户信息,完成SSO的整个过程。另外从用户体验的角度来说,如果获取不到用户信息,则意味者每次要从脸盆网访问facebook的资源,都需要重定向一次进行认证,用户体验也不好。

OAuth2与SSO

首先要明确一点,OAuth2并不是一个SSO框架,但可以实现SSO功能。以下是一个使用github作为OAuth2认证服务器的配置文件

server:

port: 11001

security:

user:

password: user # 直接登录时的密码

ignored: /

sessions: never # session策略

oauth2:

sso:

loginPath: /login # 登录路径

client:

clientId: c40fb56cb4sdsdsdsd

clientSecret: c910ec22981daa28e1b59c778sdfjh73j3

accessTokenUri: https://github.com/login/oauth/access_token

userAuthorizationUri: https://github.com/login/oauth/authorize

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

可以看到accessTokenUri和userAuthorizationUri都是为了完成OAuth2的授权流程所必须的配置,而userInfoUri则是spring security框架为了完成SSO所必须要的。所以总结一下就是:通过将用户信息这个资源设置为被保护资源,可以使用OAuth2技术实现单点登陆(SSO),而Spring Security OAuth2就是这种OAuth2 SSO方案的一个实现。

Spring Security在调用user接口成功后,会构造一个OAuth2Authentication对象,这个对象是我们通常使用的UsernamePasswordAuthenticationToken对象的一个超集,里面封装了一个标准的UsernamePasswordAuthenticationToken,同时在detail中还携带了OAuth2认证中需要用到的一些关键信息(比如tokenValue,tokenType等),这时候就完成了SSO的登陆认证过程。后续用户如果再想访问被保护资源,spring security只需要从principal中取出这个用户的token,再去访问资源服务器就行了,而不需要每次进行用户授权。这里要注意的一点是此时浏览器与client之间仍然是通过传统的cookie-session机制来保持会话,而非通过token。实际上在SSO的过程中,使用到token访问的只有client与resource server之间获取user信息那一次,token的信息是保存在client的session中的,而不是在用户本地。这也是之前我没搞清楚的地方,以为浏览器和client之间也是使用token,绕了不少弯路,对于Spring Security来说,不管是用cas、saml2还是Oauth2来实现SSO,最后和用户建立会话保持的方式都是一样的。

OAuth2 SSO与CAS、SAML2的比较

根据前面所说,大家不难看出,OAuth2的SSO方案和CAS、SAML2这样的纯SSO框架是有本质区别的。在CAS和SAML2中,没有资源服务器的概念,只有认证客户端(需要验证客户信息的应用)和认证服务器(提供认证服务的应用)的概念。在CAS中这叫做cas-client和cas-server,SAML2中这叫做Service Providers和Identity Provider,可以看出CAS、SAML2规范天生就是为SSO设计的,在报文结构上都考虑到了用户信息的问题(SAML2规范甚至还带了权限信息),而OAuth2本身不是专门为SSO设计的,主要是为了解决资源第三方授权访问的问题,所以在用户信息方面,还需要额外提供一个接口。

Authorization Server与Resource Server分离

脸盆网的这个例子中,我们看到资源服务器和认证服务器是在一起的(都是facebook),在互联网场景下一般你很难找到一个独立的、权威的、第三方的认证中心(你很难想像腾讯的QQ空间通过支付宝的认证中心去授权,也很难想像使用谷歌服务要通过亚马逊去授权)。但是如果是在公司内部,这种场景其实是很多的,尤其在微服务架构下,有大量服务会对外提供资源访问,他们都需要做权限控制。那么最合理的当然就是建立一个统一的认证中心,而不是每个服务都做一个认证中心。我们前面也介绍了,token本身是不携带用户信息的,在分离后resouce server在收到请求后,如何检验token的真实性?又如何从token中获取对应的用户信息?这部分的介绍网上其实非常少,幸好我们可以直接从官方文档获取相关的蛛丝马迹,官方文档对于resouce server的配置是这样描述的:

security:

oauth2:

resource:

userInfoUri: https://api.github.com/user

preferTokenInfo: false

寥寥数语,但已经足够我们分析了。从这个配置可以看出,client在访问resource server的被保护资源时,如果没有携带token,则资源服务器直接返回一个401未认证的错误





Full authentication is required to access this resource



unauthorized

如果携带了token,则资源服务器会使用这个token向认证服务器发起一个用户查询的请求,若token错误或已经失效,则会返回



49e2c7d8720738cfb75f6b675d62e5ecd66

invalid_token

若token验证成功,则认证服务器向资源服务器返回对应的用户信息,此时resource server的spring security安全框架就可以按照标准的授权流程进行访问权限控制了。

认证与授权的解耦

从这个流程中我们可以看出,通过OAuth2进行SSO认证,有一个好处是做到了认证与授权的解耦。从日常的使用场景来说,认证比较容易做到统一和抽象,毕竟你就是你,走到哪里都是你,但是你在不同系统里面的角色,却可能千差万别(家里你是父亲,单位里你是员工,父母那里你是子女)。同时角色的设计,又是和资源服务器的设计强相关的。从前面的配置中不难发现,如果希望获得为不同资源服务器设计的角色,你只需要替换https://api.facebook.com/user这个配置就行了,这为我们的权限控制带来了更大的灵活性,而这是传统的比如SAML2这样的SSO框架做不到的。

JWT介绍

终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。

我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。

JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种特殊格式的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:

头部Header

载荷Payload

签名Signature

这三部分均用base64进行编码,当中用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看官方文档,这里不过多赘述。

相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:

通过验证签名,token的验证可以直接在本地完成,不需要连接认证服务器

在payload中可以定义用户相关信息,这样就轻松实现了token和用户信息的绑定

在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

JWT适用场景与不适用场景

就像布鲁克斯在《人月神话》中所说的名言一样:“没有银弹”。JWT的使用上现在也有一种误区,认为传统的认证方式都应该被jwt取代。事实上,jwt也不能解决一切问题,它也有适用场景和不适用场景。

适用场景:

一次性的身份认证

api的鉴权

这些场景能充分发挥jwt无状态以及分布式验证的优势

不适用的场景:

传统的基于session的用户会话保持

不要试图用jwt去代替session。这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。这种场景下,考虑高可用,我更加推荐采用分布式的session机制,现在已经有很多的成熟框架可供选择了(比如spring session)。

scservers(六) 网关-路由

前言

前面的五篇内容已经构成了微服务的基本核心框架,已经可以结合业务,加上存储提供服务,并且整个系统内部服务之间也具备相互识别调用路由能力,那么再将这些服务汇总提供给其他第三方(不管是自己客户端(如h5,mobile app)还是作为开放平台供第三方使用),还是需要一个汇总的接口出口点。网关的需求自然形成。我们需要再提供了一个外部路由的能力。

业务网关

网关我们可以分位接入网关(连接的保持、消息的解析(卸载证书等)、消息的分发)+应用网关(路由,鉴权,限流等功能性网关的能力基础安全通用能力)。方案挺多:

* nginx(f5) + zuul(or) gateway:

nginx卸载https安全证书,高并发连接能力,反向代理能力。

gateway 功能路由鉴权,限流等包含业务功能的网关能力。

* kong(nginx基础上的包含功能性网关及业务网关能力)

nginx 我们应该用得多,比较熟悉,这里主要讲gateway的业务网关附带的一些其他能力。

sprincloud gateway

之前springcloud 一直使用zuul1.0作为其网关,后来鉴于性能(同步模型)及zuul2跳票等可能原因,社区推出了自己的网关,名字直接明了,gateway,同时还支持webflux,整合stream流等功能特性。那么我们就直接使用它来讲解吧。

  • 配置

    依赖

    <!--api网关-->
        <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--redis限流-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    属性配置(静态路由配置):

    spring: 
      cloud:
    gateway:
    discovery:
    locator:
    enabled: true
    routes:
    #网关路由到订单服务order-service
    - id: demo-ribbon-consumer
    uri: lb://center-ribbon-demo
    predicates:
    - Path=/test/ribbon/**
    default-filters:
    - name: Retry
    args:
    retries: 3
    statuses: BAD_GATEWAY
    - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    - name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1 #令牌桶的容积
    redis-rate-limiter.burstCapacity: 1 #流速 每秒
    rate-limiter: "#{@defaultRedisRateLimiter}" #SPEL表达式去的对应的bean
    key-resolver: "#{@remoteAddressKeyResolver}" #SPEL表达式去的对应的bean
  • 使用

    限流

    限流可以在nginx接入就做.gateway 也提供了限流的组件集成,基于redis做的。基本使用:

    主要查看属性配置中的:

    default-filters:
    - name: RequestRateLimiter   
    

    限流基于令牌桶算法,参数有

    * 令牌桶总容量:redis-rate-limiter.replenishRate

    * 令牌桶每秒填充平均速率:流速 每秒:redis-rate-limiter.burstCapacity

    上面两个参数只需要配置,下面两个这两个参数

    * 限流关键字对应:key-resolver:

    * 限流

    需要编写类代码:

    @Component
    public class RequestRateLimiterConfig {
    
  • 令牌桶算法

    令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

scservers(五) 服务调用——安全,熔断,限流,降级

前言

服务调用是分布式系统基础,因为微服务的理念,不管粒度如何划分,系统功能间交互调用都会复杂起来,基础服务不管什么原因挂掉,容易就造成整个系统的不可用,这就是雪崩效应。原因是服务不可用,不断重试,造成服务调用者因为同步等待造成的资源耗尽也不可用,扩大后就造成所有服务都不可用了。

解决的策略基本有如下几种:

  • 限流

    按照漏斗模型,从前向后的限流

    • 用户交互限流
    • 网关限流
    • 服务调用关闭重试
  • 改进缓存
    • 缓存预加载
    • 同步改异步刷新
  • 服务自动扩容
    • 可以在docker 容器层面解决
  • 降级

    服务调用者降级服务

    • 资源即调用服务的线程池的隔离
    • 熔断,通过阈值设置失败的服务不再继续请求
    • 快速失败:通过超时机制, 熔断器 进行快速失败
    • 快速失败根据分类可以是应答错误也可以是应答缓存or默认数据

在上述策略中,微服务系统框架可以做的主要:

  • 可用情况下预防:超时处理,进行限流,资源隔离,分类,将不重要业务降级。
  • 不可用情况下直接隔离,熔断,降级等的操作。

而Springcloud 就为我们提供了Hystrix来保护我们的系统,可以将请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。

Hystrix处理策略

当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。

  • 资源隔离

    通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池。这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。
  • 超时机制

    如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题

  • 服务限流

    通过线程池+队列的方式,通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中

  • 服务熔断

    这个熔断可以理解为我们自己家里的电闸。

    当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费

  • 服务降级

    有服务熔断,必然要有服务降级。

    所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

Hystrix配置使用

  • hystrix & ribbon

    • 配置

      依赖:

       <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
      </dependency>

      属性设置:

    • 注解式使用

      • step1:使用注解 @EnableHystrix 启用豪猪断路器功能。
      • step2:在需要降级的接口上添加注解,并添加参数类型相同的降级函数,返回自定义信息

        @HystrixCommand(fallbackMethod = "ribbonfallback")
        @RequestMapping("/test/ribbon")
        public String testconfigrpc(String name) {
        return "RestTemplate+Ribbon get username <<==>> "+restTemplate.getForObject("http://center-demo/test/config/username",String.class);
        }
        public String ribbonfallback(String name) {
        return "ribbonFallback default name :ribbonFallback";
        }
    • 监控

      host:port/hystrix

      此处只是简单的单个应用监控,显然不适合大规模集群监控,后续的多个客户端监控放到后面专门的监控章节去讲吧。

  • hystrix & feign

    • 配置

      依赖

      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
      </dependency>

      属性配置

      feign: 
        hystrix:
      enabled: true
    • 注解式使用

      • step1: 引入注解 @EnableFeignClients,其已经包含启用断路器注解
      • step2:在ribbon中我们使用注解降级函数,在feign中我们使用注解降级类

        @FeignClient( name = "center-demo", path = "/test", decode404 = true,fallback = DemoFallbackFeign.class)
        public interface DemoFeignclient {  @GetMapping("/config")
        String selectconfigByName(@RequestParam String name);
        @GetMapping("/token/back")
        String gettokenback();
        }
        @Component
        public class DemoFallbackFeign implements DemoFeignclient {
        @Override
        public String selectconfigByName(String name) {
        // TODO Auto-generated method stub
        return "FeignFallbackName";
        }
        @Override
        public String gettokenback() {
        // TODO Auto-generated method stub
        return "FeignFallbackToken";
        }
        }
  • 源码

scservers(四)服务调用——接口与负载均衡

前言

前面的三篇搭起微服务的基本应用框架:服务发现,分布式动态配置,消息总线的通知能力等。就如人的身体基本上已经有了头脑四肢,基本成形。

头脑四肢之间需要互联互通,相当于服务调用,并且需要一定的协议接口及基于协议的序列化反序列化,比如dubbo,grpc甚至原生的别人不了解的协议等。目前我们这里只介绍下http的,其他基本类同。服务的调用还要负载均衡,不然只用右手,容易起茧。

http-rest

目前http协议所使用的远程调用范式基本以REST为范式,spring3之后就提供RestTemplate来执行rest http接口调用,并提供序列化反序列的能力。但没有负载均衡,负载均衡依赖于服务注册,所以springcloud 提供了Ribbon作为软负载。

而Feign,则是及大成者,包含软件负载(ribbon)的http远程调用及序列化反序列化能力。即有如下方式:

* 原生RestTemplate+Ribbon

* Feign

RestTemplate+Ribbon

  • 配置

    我们需要添加Ribbon的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>

    但是在示例里面我们并没有在pom.xml中看到这个依赖配置,那是因为我们已经存在如下依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    前面我们说过,负载还依赖于服务注册,而如果使用eureka作为服务注册,那么其已经包含了Ribbon的依赖了。如果使用其他服务注册发现中心,如consul、zookeeper、etcd,那就要加上Ribbon的依赖。嗯,全家桶确实名不虚传。

    eureka的配置这里就不再复述了。

  • 使用

    • step1:@LoadBalanced

      在RestTemplate加上注解@LoadBalanced,让RestTemplate支持负载客户端。
    @Configuration
     class MyConfiguration {
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }
    • step2:配置负载模式

      标明Ribbon负载的工作模式,如果是轮训可以跳过这一步(默认支持轮训),如果想该用其他方式按照如下方式。已经实现了随机模式,我们也可以通过重写类支持其他模式。
    @Configuration
    public class RibbonRuleConfiguration {
    @Bean
    public IRule ribbonRule() {
    return new RandomRule();
    }
    }

    在使用到的control或者service加上使用的ribbon客户端的注解:

    @RibbonClient(name = "center-ribbon-Rule",configuration = RibbonRestTemplateConfiguration.class)
    public class AppApplication {
    
    • step3:使用 RestTemplate远程调用
    restTemplate.getForObject("http://serviceid/restpath", String.class);
    

    Eureka根据 spring.application.name 设置 serviceId,我们只要设置serviceid就可以解析出ip:port。

Feign

Feign 通过注解及拦截器让 Java HTTP 客户端编写更方便。支持可插拔编码器和解码器,降低 HTTP API 的复杂度,通过最少的资源和代码来实现和 HTTP API 的连接。通过可定制的解码器和错误处理,可以编写任意的HTTP API。Spring Cloud Feign 封装了 Ribbon 这一组件,所以在使用 Feign 同时还能提供负载均衡的功能,这一切只需要一个 @FeignClient 即可完成。

  • 配置

    新增依赖 spring-cloud-starter-openfeign

    <dependencies>
        <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
  • 使用

    1. feign client 注解引入

      在application引入 feign注解

      @EnableFeignClients
      
    2. feign client 接口

      定义rpc服务的接口

      @FeignClient( name = "center-demo", path = "/test", decode404 = true)
      public interface DemoFeignclient {
      @GetMapping("/config")
      String selectconfigByName(@RequestParam String name);
      }
    3. 调用接口

      在service中实例化该接口并使用

          @Autowired
      private DemoFeignclient feignClient;
      
      @RequestMapping("/test/feign")
      public String testconfigrpcByFeign(String name) {
      return "Feign get username <<==>> "+feignClient.selectconfigByName(name);
      }

      我们看到,所有服务提供者供外部调用的接口是一样的,如果每个不同消费者都些feign client 接口,就做了重复的工作,这就是坏味道,所以我们可以像dubbo一样在写服务提供者的时候,就可以把接口单独整个架包出来,供消费者使用。具体可见后面章节的最佳实践。

  • 拦截器

    上面是使用 Feign 来调用简单的远程服务,但实际上远程服务一般会有权限验证(这部分后续章节也会出),需要在 header 中传递 token之类的。在方法中显示传递又过于麻烦了,这时候就可以考虑使用 Feign 提供的RequestInterceptor 接口,只要实现了该接口,那么Feign每次做远程调用之前都可以被它拦截下来在进行包装。示例如下,就可以方便的传递header字段了。

    @Configuration
    public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
    requestTemplate.header("token", "ok");
    }
    }
  • 源码

  • PS注意

    Feign与显示声明的Ribbon不要混合在一个controller下使用,不然使用Feign的时候会间歇性提示404错误

          {"timestamp":"2019-03-21T05:37:41.540+0000","status":404,"error":"Not Found","message":"No message available","path":"/test/token/back"}
    

scservers(三)消息总线bus

前言

消息总线bus作为一个轻量化消息中心,通知订阅的方式,完成消息的传递,其基础的应用就是与配置中心结合,通过bus通知client来配置中心拉去更新配置。

消息总线bus

spring cloud 通过封装消息队列rabbitmq,kaffka提供我们bus的一些基础功能。发送配置更新消息。

rabbitmq

安装好rabbitmq服务端,直接使用docker-compose吧,方便。

客户端client

  • 使用bus只需要pom.xml引入如下架包配置
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
  • 同时对rabbitmq 的连接信息进行配置:
spring:
  application:
    name: bus-server
  rabbitmq:
    host: ${RABBIT_MQ_HOST:localhost}
    port: ${RABBIT_MQ_PORT:5672}
    username: ${RABBIT_MQ_USERNAME:guest}
    password: ${RABBIT_MQ_PASSWORD:guest}

即与使用其他mq方式无异。

  • 消息队列注册
@Configuration
public class RabbitMQConfig {
    @Bean
    public Queue testhelloQueue() {
        return new Queue("testhello");
    }

}
  • 消息消费
@Component
@RabbitListener(queues = "testhello")
public class ReceiverDefault {
    private final Logger logger = LoggerFactory.getLogger(ReceiverDefault.class);

    @RabbitHandler
    public void receiver(String hello){
        logger.info("接收消息=====》》》》》{}",hello);
    }
}
  • 消息推送
@Component
public class SenderDefault {
    private final Logger logger = LoggerFactory.getLogger(SenderDefault.class);
   
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sender(String context){
        
        logger.info("发送消息=========》》》》{}",context);
        this.amqpTemplate.convertAndSend("testhello",context);
    }
}

config通知

除了常用的消息队列功能,在微服务的架构中,其与config可以深度结合进行分布式的配置变更的通知。

  • 配置

    配置中心及客户端配置见配置中心一节
  • 流程

    1. 将配置通过git发布
    2. git pull 配置http接口,像配置中心发送通知

      curl -i -X POST http://confighost:port/actuator/bus-refresh
      

      应答:

      HTTP/1.1 204
      Date: Sat, 11 Aug 2018 04:06:35 GMT
      
    3. 源码示例

      center/docker-compose-demo.yaml 文件相关内容