源地址:https://www.jhipster.tech/using-uaa/
MD地址:https://github.com/jhipster/jhipster.github.io/blob/master/pages/using-uaa.md
JHipster UAA 是一个使用OAuth2认证方式的,用来保护JHipster微服务的用户账号授权的服务。
为了跟其它”UAA"(比如:Cloudfoundry UAA)进行清晰的区分,JHipster UAA是一个完全配置了OAuth2授权的服务器,包括了用户和角色,它被包装为一个普通的JHipster应用程序。这使得开发者可以深入地配置他的用户设置的每个方面,而不用被限制在现有可用的UAA的策略上。
概述
- 架构图
- 微服务架构的安全声明
- 在当前 context 下理解 OAuth2
- JHipster UAA 的使用
- 基础设置
- 理解 component
- Refresh Token
- 常见错误
- 使用 Feign client 保护不同微服务间的通信
- 使用 Eureka, Ribbon, Hystrix 和 Feign
- 使用
@AuthorizedFeignClients
- UAA 应用程序测试
- Stubbing feign clients
- 模拟 OAuth2 认证
架构图
1. 微服务架构的安全声明
在深入 OAuth2 和它在 JHipster 微服务的应用之前, 需要着重弄清楚一个安全解决方案中的声明(claims)。
1. 中心授权
由于微服务主要是关于创建独立自主的应用程序,我们想要一个一致的认证体验,以使到用户感觉不到他的请求是由不同的应用提供的,而这些应用也可能用到了独立的安全配置。
2. 无状态
创建微服务的主要好处是可扩展性。所以安全解决方案的选择是不应该影响这个好处的。在服务器中保持用户的 session 状态变得棘手,在这个情形下应优先使用无状态(stateless)的解决方案。
3. 用户/机器 的请求区分
不同用户或者不用机器有可能需要进行清楚的区分。使用微服务架构会导致创建一个大型的关于不同域和资源的多个用途的数据中心,因此有必要限制不同的客户端(比如:原生app,多个单网页程序(SPA))的请求。
4. 精细的请求控制
当维护中心化的角色时,有时有需要在每个微服务配置详细的请求控制策略。一个微服务不应该具有识别用户的能力,而是关注于授权进来的请求。
5. 防御攻击
不管哪种安全方案能解决多少的问题,它都应该尽可能地少漏洞。
6. 可扩展性
使用无状态协议(stateless protocols)的安全方案时应保证可扩展性,不应该存在单个点上面的失败。一个反面例子就是一个共享的认证数据库或一个单独的认证服务器实例,这样每个请求对应一个访问。
2. 在当前 context 下理解 OAuth2
使用 OAuth2 协议(注意: 它是一个协议(protocol), 不是一个框架, 不是一个应用) 满足了所有这6个声明(claims)。它遵循严格的标准,这使得这种方案与其它微服务和远程系统兼容。 JHipster 提供了数种方案,都基于以下的安全设计:
- 对于该架构下的每个端的请求都是通过一个客户端(client)
- 一个客户端(client)是指一个像 "Angular $http client" ,某些"REST-Client", "curl",或者任何可以发起请求的客户端
- 一个客户端(client)也可以与其它的用户认证方式组合使用,如前端 Angular 中的 $http
- 每个提供资源服务的微服务(包括 UAA)都是资源服务器(resource servers)
- 上图中的蓝色箭头表示客户端向 Oauth 服务器发起认证请求
- 上图中的绿色箭头表示客户端向资源服务器发起已受权的请求
- UAA 服务器是一种包括了认证和资源服务的服务器
- UAA 服务器是所有微服务应用数据的所有者(它自动授权了每个对资源服务器的请求)
- 提供了用户认证的客户端请求,是通过 client ID 和网关中文件配置的 secret 使用 "password grant" 方式进行授权的
- 客户端请求资源时如果没有用户,是使用 "client credentials grant" 方式进行授权的
- 每个客户端都要在 UAA (web-app, internal, ...)中定义
此设计应该适用于任何独立的语言或框架的微服务架构。
此外,以下的规则可以应用于请求控制。
- 使用 "roles" 和 RBAC 配置的用户请求
- 使用 "scopes" 和 RBAC 配置的机器请求
- 使用 ABAC 表达的和使用包括了 "roles" 和 "scopes" 的 boolean 表达式的复杂请求
- 例如: hasRole("ADMIN") and hasScope("shop-manager.read", "shop-manager.write")
3. JHipster UAA 的使用
当组建一个 JHipster 微服务时,你可以不选择 JWT 认证而去选择 UAA选项。
注意: UAA 方案其实也用到了 JWT,它也可以像使用默认的 Spring Cloud Security 那样使用自定义的 JWT 配置。
基础设置
基础设置包括了:
- 一个 JHipster UAA 服务器(类型为application)
- 至少一个其它的使用了 UAA 认证的微服务
- 一个使用了 UAA 进行认证的 JHipster 网关
需要按以上的顺序来创建这3个应用
除了认证类型(authentication type),UAA 的位置也必须提供。
对于基础的用法,这种设置与 JWT 认证方式一样地工作,只不过多了一个服务。
理解 component
JHipster UAA 服务器自动做了以下3种事情:
- 它提供了默认的用户域,包括用户和账号资源(这是通过网关中 JWT 认证实现的)
- 它为 OAuth2 实现了
AuthorizationServerConfigurerAdapter
并定义了基础客户端 ("web_app" 和 "internal") - 它通过
/oauth/token_key
URI 提供 JWT 公共 key 服务, 其它微服务也通过该 URI 进行请求。
JHipster 并不限定开发者使用哪种的数据库、缓存方案、搜索引擎。
当一个微服务启动时,它通常认为 UAA 服务器已经可以提供它的公共 key。该微服务会请求 /oauth/token_key
获得公共 key 并使用它来配置 key 签名 (JwtAccessTokenConverter
)。
如果 UAA 没有启动,微服务应用会尝试等一会启动并获取公共 key。uaa.signature-verification.ttl
属性用来设置 key 的可用时间,之后会重新获取。uaa.signature-verification.public-key-refresh-rate-limit
属性用来限制对 UAA 的请求次数,以避免过多请求。这些属性通常使用默认值就可以了。任何情况下,如果认证失败了,微服务会检查是否有新的 key。这样,对于 UAA 的 key 更改,所有微服务会自动重新获取。
从这一点出发,这种基础设置有两种使用情况:用户调用和机器调用。
对于用户调用,可以通过访问 /auth/login
发起登录请求,然后在 /auth/login
中使用OAuth2TokenEndpointClientAdapter
发起一个密码验证方式的请求到 UAA 认证服务。 因为这个请求是在网关发生的, client ID 和 secret 并没有保存在任何的客户端代码中,用户也获取不到它们。网关会返回一个包括 token 的 Cookie,然后客户端的每个请求都发送这个 Cookie 即可。
对于机器调用,该机器需要使用一个 UAA client credentials grant 方式的请求进行认证。JHipster 提供了一个标准的方案,在 使用 Feign client 保护不同微服务间的通信 中有具体的说明
Refresh Token
刷新请求 token 的一般流程在网关中发生,如下:
- 认证是通过
AuthResource
调用OAuth2AuthenticationService
的 authenticate 来设置 Cookie 的. - 对于每个请求,
RefreshTokenFilter
(由RefreshTokenFilterConfigurer
配置) 会检查请求 token 是否过期或它有没有一个有效的刷新 token - 如果有,它通过
OAuth2AuthenticationService
进行刷新 token. - 这使用了
OAuth2TokenEndpointClient
接口来发送一个 OAuth2 服务器认证的刷新 token,我们例子中是 UAA 服务器 (通过UaaTokenEndpointClient
). - 刷新认证后的结果会被下载到客户端用作新的 cookie。
常见错误
以下列表是开发者需要认识到的主要事情。
在生产环境和开发阶段使用相同的签名 key
强烈建议尽可能地使用不同的签名 key。如果签名 key 被别人得到的话,他很可能能使用它来创建任何用户的登录证书。
没有使用 TLS
如果攻击者拦截到请求 token 的话,在这个 token 失效前,他将获得这个 token 被授权的所有权限。有太多拦截请求 token 的方式了,比如没有使用 TLS 加密。 OAuth 第一个版本时并没有问题,因为那时协议层是强制加密的。
在 URL 中使用请求 token
作为标准,请求 token 可以放到 URL 、header 或者 cookie中。从 TLS 角度来看,三种方式都是安全的。实际上放到 URL 是没那么安全的,因为有几种方式能从历史记录中获取到 URL。
切换到对称签名 key
RSA 并没有被 JWT 签名强制使用,Spring Security 也提供了 token 对称签名. 这解决了一些问题,也导致开发麻烦。但这是不安全的,因为攻击者只需要进入单个微服务就可以生成它的 JWT token 了。
4. 使用 Feign client 保护不同微服务间的通信
当前只有 JHipster UAA 提供了一个不同微服务间安全通信的可扩展的实现。
使用 JWT 认证时,如果在请求中发起内部请求时没有手动转发(forwarding) JWT,这会强制微服务通过网关调用其它的微服务,这会导致冗余的内部请求。但即使使用了转发(forwarding),也不能清楚地区分是用户还是机器的认证。
由于 JHipster UAA 基于 OAuth2,这些问题都在协议定义中被解决了。
本章节包含如何解决这些问题。
使用 Eureka, Ribbon, Hystrix 和 Feign
当一个服务需要从另一个服务请求数据时,这4个东西都要被用到。所以需要知道这4个东西各自的职责是什么:
- Eureka: 这是服务注册(取消注册)的地方,所以你可以通过 Eureka 来获取已经被注册到 Eureka中的 "foo-service" 服务和它实例的一组 IP
- Ribbon: 当有人请求 "foo-service" 并已经拿到了该服务的一组 IP 时, Ribbon 通过对这些 IP 进行负载均衡.
例如 ,有两个 JHipster UAA 服务器的实例分别运行在 IP 10.10.10.1:9999 和 10.10.10.2:9999 上,我们想访问 URL "http://uaa/oauth/token/",我们可以使用一个叫做 Round Robin 的算法让 Eureka 和 Ribbon 快速将这个 URL 转换为 "http://10.10.10.1:9999/oauth/token" 或者 "http://10.10.10.2:9999/oauth/token"。
- Hystrix: 一个断路开关系统,它可以用来处理服务失败的回退情况。
- Feign: 通过声明的方式进行使用
实际情况上,不可能保证所有服务的所有实例都在运行。所以 Hystrix 像一个断路开关,在已经定义好的方式下用来处理失败情况下的回退。
但是组合和开发这些东西包括了很多工作:对于已经在 Eureka 中注册的服务,Feign 提供了编写 Ribbon 均衡负载 REST 客户端的方式,包括使用 Hystrix 控制的回退实现,而这只需要写一些 Java 接口的注解。
Feign client 对于服务间的通信是非常有用的。当一个服务需要一个 REST 客户端来请求另一个叫"other-service"服务中的"other-resource"时,可以用以下方式来声明一个接口:
@FeignClient(name = "other-service")
interface OtherServiceClient {
@RequestMapping(value = "/api/other-resources")
List getResourcesFromOtherService();
}
然后,通过依赖注入来使用:
@Service
class SomeService {
private OtherServiceClient otherServiceClient;
@Inject
public SomeService(OtherServiceClient otherServiceClient) {
this.otherServiceClient = otherServiceClient;
}
}
与 Spring Data JPA 类似,无需实现该接口。如果使用 Hystrix时,你也可以实现它,在 Feign client 接口的实现类中实现回退。
一个未解决的问题是,如何使用 UAA 来确保这个通信的安全。想达到这个目的,Feign 需要一些请求拦截器(request interceptor),而这些拦截器需要实现 OAuth 中的授权当前服务去请求其它服务的客户认证流程。在 JHipster 中你只需使用 @AuthorizedFeignClients
注解来达到这个目的。
使用 @AuthorizedFeignClients
如果想让以上的 Feign client 在一个叫 "other-service" 的服务中使用,这个服务下的资源是受保护的,那么接口需要加上如下注解:
@AuthorizedFeignClient(name = "other-service")
interface OtherServiceClient {
@RequestMapping(value = "/api/other-resources")
List getResourcesFromOtherService();
}
注意: 由于 Spring Cloud 中的一个 bug,目前不可以使用以下的注解方式:
@AuthorizedFeignClient("other-service")
或者
@AuthorizedFeignClient(value = "other-service")
这样,即使内存中没有有效的请求 token时, REST client 也会自动被你的 UAA 服务器认证。
这种实现方式处理了一种“一个没有指向用户 session 的单独 OAuth 客户端发起机器请求”的情况。这是重要的,特别是当一个请求对另一个服务的请求的 entity 进行验证时。可选地,最初请求的请求 token 也可以转发。目前,JHipster 没有提供默认的方案。
5. UAA 应用程序测试
Feign client 模拟
与 Feign client 使用的组件应该是可以被测试的。在测试和生产环境使用一样方式的 Feign,需要令到 JHipster Registry 和 UAA 服务器在同一部进行测试的机器中可用,但大多数情况下,你不想测试 Feign 本身是否正常(通常都是正常的),而是测试使用了 Feign 的 component 是否正常。
测试使用 Feign 的 component 时,可以使用 @MockBean
,它在 spring boot 1.4.0 以上版本已存在.
以下例子,用来测试 SomeService
是否正常运行,在 client 中设置了模拟值:
@RunWith(SpringRunner.class)
@SpringBootTest(App.class)
public class SomeServiceTest {
@MockBean
private OtherServiceClient otherServiceClient;
@Inject
private SomeService someService;
@Test
public void testSomeService() {
given(otherServiceClient.getResourcesFromOtherService())
.willReturn(Arrays.asList(new OtherResource(...));
someService.performActionWhichInkvokesTheAboveMentionedMethod();
//assert that your application is in the desired state
}
}
使用这种方式,你在模拟其它服务的行为,并提供了从源地址期望一致的资源 entity。
所有注入了 client 的 bean 会表现为模拟的,所以你只需关注这些 bean 本身的逻辑。
模拟 OAuth2 认证
使用 Spring 的集成测试时,REST controller 通常会跳过 security 的配置,因为它令到测试变得困难,因为主要的目的是证明 controller 的功能是否正常。但有时,测试 controller 的 security 行为也是测试的一部分。
对于这种情况,JHipster 提供了一个叫做 OAuth2TokenMockUtil
的 component,它可以在没有用户或者 client 存在时来模拟一个有效的认证。
想要使用这个功能,需要做以下两件事:
1. 在 Spring MVC context 中启用 security 并将 mock util 注入。
@Inject
private OAuth2TokenMockUtil tokenUtil;
@PostConstruct
public void setup() {
this.restMockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
在本测试中只模拟应用程序的 WebApplicationContext
2. 使用 OAuth2TokenMockUtil
该 util 提供了一个 "oaut2authentication" 方法,通过 MockMvc 的 with 方式来使用。目前它可以配置 authentication 的以下字段:
- username
- roles (Set
) - scope (Set
)
具体例子:
@Test
public void testInsufficientRoles() {
restMockMvc.peform(
get("url/requiring/ADMIN/role")
.with(tokenUtil.oauth2Authentication("[email protected]", Sets.newSet("some-scope"), Sets.newSet("ROLE_USER")))
).andExpect(status().isForbidden());
}