微服务应用安全——Security
对于业务应用安全,主要考虑两方面:一方面是保障只有认证的用户才可以访问应用,也就是用户认证;另一方面就是要保障访问者只有拥有足够的权限才可以访问某个资源,也就是用户鉴权。
见SecurityConfig,在用户鉴权的configure方法中,antMatchers的命名来源于Java早期的一个构建工具Ant,用于路径匹配,在这里可以使用通配符也可以使用逗号隔开多个路径。
10.2 微服务安全
如何提供细粒度的安全管控方案?
在进行单体架构应用开发时最常使用的安全管理机制就是基于用户会话(Session)的认证,用户登录成功后,服务器就会将用户相关数据及权限信息存储到Session中。通常Session会直接保存在应用服务器中,仅将Session的ID返回到客户端,并存储在浏览器的Cookie中(对于Java应用来说就是jsessionid),当下次访问时就可以通过该ID获取到相应的会话信息。当使用服务器集群时,可以通过Session复制或Session粘滞的方法来保障服务器可以获取到相应的Session数据。
1.单点登录(SSO)方案
单击登录方案是最常见的解决方案,但单点登录需要每个与用户交互的服务都必须与认证服务进行通信,这不但会造成重复,也会产生大量琐碎的网络流量;
2.分布式会话(Session)方案
通过将用户会话信息存储在共享存储中,如Redis,并使用用户会话的ID作为key来实现分布式哈希映射。当用户访问微服务时,会话数据就可以从共享存储中获取。该解决方案在高可用和扩展方面都很好,但是由于会话信息保存在共享存储中,所以需要一定的保护机制保护数据安全,因此在具体的实现中会具有比较高的复杂度。
3.客户端令牌(Token)方案
令牌由客户端生成,并由认证服务器签名。在令牌中会包含足够的信息,客户端在请求时会将令牌附加在请求上,从而为各个微服务提供用户身份数据。此方案解决了分布式会话方案的安全性问题,但如何及时注销用户认证信息则是一个大问题,虽然可以使用短期令牌并频繁地与认证服务器进行校验,但并不可以彻底解决。JWT(JSONWeb Tokens)是非常出名的客户端令牌解决方案,它足够简单,并且对各种环境支持程度也比较高。
4.客户端令牌与API网关结合
通过在微服务架构中实施API网关,可以将原始的客户端令牌转换为内部会话令牌。一方面可以有效地隐藏微服务,另一方面通过API网关的统一入口可以实现令牌的注销处理。
第二个方案:分布式Session方案中要求开发者能够将用户会话信息单独拎出来进行集中管理。业界比较成熟的开源项目有Spring Session,其使用Redis数据库或缓存机制来实现Session存储,并通过过滤器实现Session数据的自动加载。
基于令牌认证通常包含下面几层含义:
·令牌是认证用户信息的集合,而不仅仅是一个无意义的ID。
·在令牌中已经包含足够多的信息,验证令牌就可以完成用户身份的校验,从而减轻了因为用户验证需要检索数据库的压力,提升了系统性能。
·因为令牌是需要服务器进行签名发放的,所以如果令牌通过解码认证,我们就可以认为该令牌所包含的信息是合法有效的。
·服务器会通过HTTP头部中的Authorization获取令牌信息并进行检查,并不需要在服务器端存储任何信息。
·通过服务器对令牌的检查机制,可以将基于令牌的认证使用在基于浏览器的客户端和移动设备的App或是第三方应用上。
·可以支持跨程序调用。基于Cookie是不允许垮域访问的,而令牌则不存在这个问题。
综上:可以通过验证令牌来完成用户身份的校验。基于令牌的这个优点,像T微信、支付宝、微博及GitHub等,都推出了基于令牌的认证服务,用于访问所开放的API及单点登录。接下来将重点介绍基于令牌认证方案中的OAuth 2.0和JWT。
10.3 基于OAuth 2.0的认证
OAuth是一个开放的、安全的用户认证协议,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源,而无须将用户名和登录口令提供给第三方应用。授权的第三方应用只能在特定的时段内访问特定的资源,而非所有内容。每次授权的令牌只能针对一个第三方应用,因此可以认为OAuth是一个非常安全的用户认证/授权协议。
OAuth 2.0对认证流程进行了简化,更加关注客户端开发者的简易性,并为Web应用、桌面应用、手机App等提供专门的认证流程。但OAuth 2.0并不向下兼容OAuth 1.0。
基于OAuth 2.0的用户认证具有以下优点。
简单:安全:并不涉及用户密钥等信息 。开放:OAuth 2.0只是一个用户认证安全协议,所以任何服务提供商都可以基于该协议来实现,任何软件开发商都可以使用该协议完成用户认证流程。
10.3.1 OAuth 2.0授权流程
(1)用户打开客户端以后,客户端要求用户给予授权。(2)用户同意给予客户端授权。(3)客户端使用上一步获得的授权,向认证服务器申请令牌。(4)认证服务器对客户端进行认证以,确认无误后同意发放令牌。(5)客户端使用令牌,向资源服务器申请获取资源。(6)资源服务器确认令牌无误,同意向客户端开放资源。
涉及到的4种角色:
·资源拥有者:资源拥有者是对资源具有授权能力的人,通常也就是我们所说的用户。·客户端/第三方应用:代表资源所有者对资源服务器发出访问受保护资源的请求。·资源服务器:资源所在的服务器,也就是受安全认证保护的资源。·授权服务器:就是通常所说的认证服务器,为客户端应用程序提供不同的访问令牌。授权服务器可以和资源服务器在统一服务器上,也可以独立部署。
10.3.2 客户端授权模式
定义了4种客户端授权模式,分别是授权码模式(Authorization Code)、简化模式(Implicit)、密码模式(Resource Owner Password Credentials)和客户端模式(Client Credentials)。其中,最后一种客户端模式是指客户端以自己的名义而不是用户的名义向授权服务器进行认证,实际上并不存在用户授权问题
1.授权码模式(Authorization Code)
授权码模式是OAuth 2.0中功能最完整、流程最严密的授权模式。
(1)用户访问客户端,客户端将用户引导到授权服务器上。(2)用户选择是否同意给客户端授权。(3)如用户同意授权,授权服务器将重定向到客户端事先指定的地址,同时附加上一个授权码(Token)。(4)客户端收到授权码后,同时附加上需要重定向的页面(如果有的话),经由客户端后台向授权服务器申请令牌。(5)授权服务器校验授权码后,向客户端发送访问令牌(Access Token)和更新令牌(Refresh Token)并重新定向到上一步指定的页面。
2.简化模式(Implicit)
简化模式是指不通过客户端的后台服务器来获取访问令牌,这里的客户端通常是浏览器,客户端直接通过脚本语言(一般是JavaScript)来完成向授权服务器申请访问令牌的操作。具体流程如下:
(1)用户访问客户端,客户端将用户引导到授权服务器上,并附加认证成功或失败时需要重定向的URI。(2)用户选择是否同意给客户端授权。(3)如用户同意授权,那么授权服务器根据user-agent中的数据进行验证,验证通过后将用户重定向到之前所指定的地址,同时在所重定向的地址中附加一个相应访问令牌的值;(4)浏览器将返回的信息保存在本地,然后向资源服务器发出请求,但不包括访问令牌。(5)资源服务器返回一个网页,通常在该网页中会包含一段代码,该代码可以获取之前返回的访问令牌。(6)浏览器执行上一步中获得的脚本,并获取到访问令牌。7)浏览器将解析到的访问令牌发送给客户端。
3.密码模式(Resource Owner PasswordCredentials)
密码模式是指客户端通过用户提供的用户名和密码信息,直接通过授权服务器来获取授权。在这种模式下,用户需要把自己的用户名和密码提供给客户端,但是客户端不得储存这些信息。该模式只有在用户对客户端高度信任的情况下或者同一个产品系列中,在实际生产中应避免使用这种授权模式。该模式的授权流程如下:
(1)用户向客户端提供相应的用户名和密码。(2)客户端通过用户提供的用户名和密码向授权服务器请求访问令牌。(3)授权服务器确认后,返回访问令牌给客户端。
10.3.3 使用OAuth 2.0完成用户认证及授权
1.搭建一个基于OAuth 2.0的认证服务器,完成用户认证及授权功能
2.完成商品微服务权限认证的迁移
3.了解微服务之间调用时认证信息是如何进行传递的,对用户微服务和前面所建立的API网关服务器(Zuul)进行改造,增加权限控制处理
1.添加依赖
这两个依赖中的spring-cloud-security将会给工程中增加安全认证所需要的基础库,而spring-security-oauth2则是在上述的基础上增加OAuth 2.0认证体系所需要的库。
引导类添加@EnableAuthorizationServer,该注解一是将当前应用作为一个OAuth认证服务,二是为应用增加了一系列的REST端点,从而提供一个标准的OAuth 2.0的实现。
2.开发过与OAuth集成应用的读者都知道,当需要和第三方认证集成时通常要提供一个ClientID(或AppID)和ClientSecret(或AppSecret)用来进行认证。
具体实现方式就是需要扩展Spring的AuthorizationServerConfigurerAdapter,并覆写其中的configure()方法。
工程auth-server,见OAuthConfig,
在上面的代码中我们使用内存方式直接注册了一个ClientID为springclouddemo,客户端的secret为scdsecret,并且授权该客户端可以使用refresh_token、password、client_credentials等的客户端授权方式。
3.当客户端创建完毕后,就可以完成具体用户的认证和权限授权处理了,见security-oauth下auth-server,OAuthWebSecurityConfigurer;
其中**,WebSecurityConfigurerAdapter用于保护OAuth相关端点,同时用于用户登录;而ResourceServerConfigurerAdapter则用于保护OAuth要开放的资源,同时主要作用于客户端及令牌的认证**。如果不调整顺序的话,可能会造成没有权限访问的异常。此外,我们也可以通过在上面两个Adapter中配置需要开放的权限验证路径来达到目的。
最后还有一个最重要的功能需要实现,就是认证服务器需要提供一个可以根据访问令牌获取认证用户相关信息的端点,通常为/auth/user。这个功能借助Spring MVC和Spring Security可以轻松完成,见Application下user实现;
4.通过请求http://localhost:8900/auth/oauth/token,就可以获取到一个访问令牌了。
然后就可以使用该访问令牌通过认证服务所提供的用户信息端点http://localhost:8900/auth/user获取相关的用户认证信息了
5商品微服务改造 工程代码:cd826dong-cluds-security
1)替换之前的security依赖,为spring-cloud-security,spring-security-oauth2
2)配置文件添加获取用户认证信息地址的信息
security.oauth2.resource.user-info-uri=http://localhost:8900/auth/user
3)引导类中增加**@EnableResourceServer**注解,该注解告诉Spring Boot这应用是一个OAuth保护资源,在访问时Spring Security就会从请求信息中检查是否有合法的访问令牌(Access Token),并从该令牌所配置的认证用户信息端点中获取相应的用户信息,获取成功后SpringSecurity就会将用户信息存放到Spring安全上下文中,以便后面对用户进行鉴权。
4)将原来的安全配置类SecurityConfig删除,并替代为下面新的安全配置类:
见product-server下,config中的类ResourceServerConfiguration
5)在Postman中可以通过图10-11所示的方法将上一步在认证服务器所得到的访问令牌设置到Header的Authorization中,然后再次访问即可。
通过商品微服务访问用户微服务还会报错,原因:我们平常在调用其他微服务时是通过默认的RestTemplate进行调用,该类并不会自动处理OAuth认证的相关信息,需要将微服务调用更改成使用OAuth2RestTemplate类,该类在发起请求时可以自动将原来请求中所包含的访问令牌进行转发。
在商品Application中添加OAuth2RestTemplate实现。
10.3.4 整合API网关服务
一般而言,商品微服务不会像上面直接访问用户服务,而是通过之前我们所搭建的统一API网关服务来进行访问,见商品微服务调用:
很不幸,这里的代码更改并没有为我们带来预想的结果,而是抛出了用户未认证异常的错误。这是为什么?前面在介绍Zuul配置时有一个敏感Header的设置,默认情况下会过滤Cookie、Set-Cookie和Authorization这3个。当商品微服务通过Zuul调用用户微服务时就会将OAuth2RestTemplate转发的Authorization给过滤掉,所以用户微服务将收不到用户访问令牌,从而抛出未认证异常错误。
只需要在Zuul服务器中重设一下敏感Header就可以了:
zuul.sensitiveHeaders=Cookie,Set-Cookie
当我们使用Zuul实施了统一的API网关服务之后,还有一个更简便的方式来启用OAuth2的单点登录,就是在Zuul服务器中增加一个**@EnableOAuth2Sso**注解,并在配置文件中增加类似如下的配置:
spring.oauth2.client.clientId=bd1c0a783ccdd1c9b9e4 spring.oauth2.client.clientSecret=1a9030fbca47a5b2c28e92f19050bb77824b5ad1
spring.oauth2.client.accessTokenUri=
https://github.com/login/oauth/access_token
spring.oauth2.client.userAuthorizationUri=
https://github.com/login/oauth/authorize
spring.oauth2.client.clientAuthenticationScheme=form
spring.oauth2.resource.userInfoUri=https://api.github.com/user
spring.oauth2.resource.preferTokenInfo=false
这样Zuul服务器就可以自动登录到单点认证服务器(如上面所设置的GitHub),并转发到用户登录界面,用户认证后就可以获取到相应的访问令牌,并将令牌传递到下游服务。如果下游的服务在引导类中注解了@EnableResourceServer,那么就会从Header中获取该令牌。此时也不需要再在Zuul配置文件中指定敏感Header设置了。
10.4 基于JWT的认证
JWT是基于JSON的一个开放标准-RFC7519,其代表的是一种紧凑的、URL安全的、能够在网络应用间传输的声明。当我们使用在认证场景下时,JWT可以用来在客户端和资源服务器间传递认证用户的身份信息,以便于从资源服务器获取某个资源。同时,为了资源服务器认证及业务需要,也可以在令牌中增加一些额外信息。
一个标准的JWT通常看起来如下,格式为header.payload.signature
·头部(header):头部描述了该JWT的最基本信息,如类型、签名算法等。
·载荷(payload):载荷存放了令牌有效信息。一个有效信息通常包含3个部分。
✧ 标准中注册声明:这部分是对令牌中的一些标准属性信息进行声明,标准规定这是一个建议不强制。常用的属性信息有:iss:令牌的签发者;sub:令牌所面向的用户;aud:接收令牌的一方;iat:令牌签发时间;exp:令牌过期时间;nbf:定义令牌有效起始时间,在该时间之前是不可用的;jti:令牌唯一身份标识,主要用来作为一次性令牌,避免重放攻击。
✧ 公共声明:该部分一般是用户相关信息或业务需要的其他信息,对于该部分的信息没有限制,但不建议将敏感信息放在该部分,因为客户端可以对该部分进行解密。
✧ 私有声明:该部分是提供者和消费者所共同定义的声明,也是使用Base 64进行加/解密,因此也不建议存放敏感信息。
·签名(signature):将头部和载荷使用Base 64编码后,通过所使用的加密方法进行签名,签名后的结果放在这部分内容中。
当使用JWT用户认证时,客户端与资源服务器及认证服务器之间的交互流程如下:
(1)客户端调用认证服务器的登录接口/获取Token接口,传入用户名密码。(2)认证服务器确认用户名密码,并创建JWT返回给客户端;(3)客户端获取到JWT后进行缓存。(4)客户端请求资源服务器,并在请求的HTTP头部中附加JWT。(5)资源服务端对JWT进行校验,校验通过后,向客户端返回相关的资源和数据。
使用JWT进行认证处理具有以下优点:
·JWT是基于令牌的,将用户状态分散到了客户端中,服务器端无状态,减轻了服务器的压力,提升了性能;
·JWT具有严格的结构化,其自身就包含了关于认证用户的相关消息,一旦校验成功那么资源服务器就无须再去认证服务器验证信息的有效性;
·JWT中的载荷可以支持定制化,因此开发者可以根据业务需要进行扩展定义,如添加用户是否是管理员、用户所在分桶等信息,从而满足业务需要;
·JWT体积小,便于传输,并且在传输方式可以支持URL/POST参数或者HTTP头部等方式传输,因而可以支持多种客户端,不仅仅是Web;
·JWT使用JSON格式,对跨语言的支持非常好;
·JWT支持跨域,使单点登录的开发更容易。
10.4.1 改造认证服务支持输出JWT
1.添加依赖spring-security-jwt,用户微服务和商品微服务都需要增加该依赖
2.配置好上面的依赖之后,就可以告诉认证服务器将所生成的访问令牌转换成JWT令牌了。
定义一个令牌转换器JWTTokenConfig
上面的代码中最主要的就是创建了一个JwtAccessTokenConverter的Bean。这里我们设置了一个JWT加密所使用的密钥。对于JWT的加密,支持对称加密和非对称加密,非对称加密需要在服务端生成一个密钥对(公钥和私钥),每一个需要使用JWT的客户端都可以获取到公钥,并可以使用公钥对JWT进行解密和认证。对于公钥和私钥的生成需要一个证书,相对来说比较麻烦,示例中直接采用对称加密的方式进行演示,有兴趣的读者可以使用JDK自带的keytool工具完成,这里就不再详细介绍了。
对于对称加密,只需要配置一个密钥就可以了,但需要注意的是不论服务器端还是客户端需要使用到JWT的项目都需要知道该密钥。作为示例,这里就直接将密钥存放在配置文件中,但在实际生产环境中建议开发者还是将密钥统一存放在配置服务器中,不要在每个项目中进行配置。
jwt.signing.key = springcloud-dong
该密钥通过Spring Boot的自定义扩展属性,通过@Value("${jwt.signing.key}")注解直接将值注入到ServiceConfig中。同样,在后续用户微服务、商品微服务及Zuul中都需要配置该属性。
3.当创建了JwtAccessTokenConverter Bean之后就可以更改OAuth的配置,使其在生成访问令牌时可以使用该转换器进行转换,这个需要调用前面的OAuthConfig.configure(),具体修改后的代码如下:见方法configure;
在上面的代码中,将访问令牌的转换器设置为之前所创建的JwtAccessTokenConverter,这样所生成的令牌就是一个标准的JWT令牌了。
http://localhost:8900/auth/oauth/token,获得的令牌
http://localhost:8900/auth/user就可以获取到相应的认证用户及其授权信息
我们可以借助一个在线工具https://jwt.io来对前面的令牌数据进行解码并进行校验。
当我们将所使用的密钥springcloud- dong输入到VERIFY SIG NATURE中的输入框后,在最下面就可以看到签名验证成功的提示。
如何进行私有属性的扩展?
首先看一下JwtTokenConfig类的定义,见类JWTTokenEnhancer,在类中增加了属性;
然后在OAuthConfig中通过下面的代码将该增强器设置到访问令牌增强器链中,这样当生成一个访问令牌时,就会调用以下增强代码对访问令牌中的属性进行扩展了。
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(this.jwtTokenEnhancer,
this.jwtAccessTokenConverter));
10.4.2 在Zuul中对JWT进行解析
前面讲过JWT具有严格的结构化,其自身包含了关于认证用户相关的消息,一旦校验成功,资源服务器就不需要再次进行认证,可以直接使用这些信息。
从JWT中已经可以解析到用户名、用户权限及开发者自己所扩展的店铺ID信息,一个比较好的思路就是通过所搭建的API网关,由该网关统一进行解析,解析之后就可以将这些数据转发给下游各微服务之间使用。
步骤:
1.添加依赖io.jsonwebtoken jjwt
2.编写过滤器
见zuul-server里JWTTokenFilter,
这是一个pre类型的过滤器,在对JWT令牌的解析中,我们判断HTTP头部中是否存在Authorization属性,如果有那么尝试使用JJWT所提供的工具进行解析,解析出来的结果通过FilterUtils存放到Zuul请求的头部中。这里仅解析出需要的shopId和user_name两个属性
注意:为了保证能够正确解析出来,一定要在配置文件中将密钥配置进去。
10.4.3 改造商品微服务
在单体架构开发时我们常常会创建一个类UserContext,该类中包含了当前用户的上下文信息,同时会增加一个过滤器对所有请求初始化用户上下文信息。
使用Zuul对JWT令牌统一进行解析并转发到下游应用:见UserContextFilter,在商品微服务中增加用户上下文处理
http://localhost:8280/productservice/products/3;
如果通过商品微服务的端口直接访问,因为没有进行解析,所以会输出Current shop.id = null;
如果此时通过Postman访问http://localhost:8280/productservice/products/3/comments,很不幸会得到一个异常信息,这是为什么呢?前面说过,当在商品微服务中使用OAuth2 RestTemplate进行微服务请求时,会自动转发OAuth认证服务器所生成的访问令牌,但是当将访问令牌转换成JWT令牌时,OAuth2RestTemplate就会拒绝转发。此时需要手工做一些调整才可以。
1首先需要在商品微服务中增加一个用户请求拦截器用来对JWT令牌进行转发。
商品微服务中见JWTOAuthTokenInterceptor。
2然后修改调用微服务所使用的RestTemplate,商品微服务Application见getCustomRestTemplate实现,
3同时见UserServiceImpl实现中 @Autowired
private OAuth2RestTemplate restTemplate;改为 @Autowired
private RestTemplate restTemplate;
4重启访问商品评论服务即可;
实施JWT安全方案缺点:
·JWT令牌注销:由于JWT令牌存储在客户端,当用户注销时可能由于有效时间还没有到,造成客户端还会存储,这时候需要开发者能够有效防止注销后令牌的访问,开发者可以借助API网关来实现。另外,采用短期令牌也是一个不错的解决方案。
·JWT令牌超长:
·避免成为系统新瓶颈:由于API网关服务会对认证服务器进行访问及鉴权处理,有可能会形成系统的新瓶颈。
·需有效防范XSS攻击:由于JWT存储在客户端,最有可能引发XSS攻击,因此当使用JWT时必须做出有效的防范。