<微服务安全架构与实践@杨波>学习笔记
微服务安全要解决什么问题
1.开放系统间授权
不适用于开放系统间
developer key 可能泄露
与OAuth2比较接近, 需要考虑如何管理, 颁发令牌
2.现代微服务安全
- 服务粒度很小, 服务和服务之间怎么去认证和鉴权
- 应用的形态多种多样, 不只是浏览器, 还有单页应用, 原生APP, 服务器端APP
独立的服务AuthServer, 并且把认证和授权都做成一个Service … 基于Token的授权
3.总结
OAuth2的定义和原理
令牌: AccessToken : 由授权服务器颁发
OAuth2的正式定义
令牌 类比 仆从钥匙(Valet Key)
给应用授权有限的访问权限, 让应用能够代表用户去访问用户的数据
OAuth 2.0 优势
不足
主要角色
OAuth2 术语
令牌类型
误解
总结
OAuth2 的流程和选型
典型的OAuth Flow
1. 授权码模式
2. 简化模式
3. 密码模式
用户名和密码尽量不要泄露?
Client 不是第三方的, 是自己的, 风险不是特别大
4. 客户端模式
刷新令牌流程
OAuth Flow 选型
授权流程渠道
没有资源服务器参与的: 前端渠道
没有资源拥有者参与的: 后端渠道
客户应用类型
公开应用: 客户可以直接接触这些应用, 公开的, 不能存储重要的客户凭证信息 (客户密码), 可能会泄露
私密应用: client 在后端, 可以存储客户的凭证 (密码信息)
四种OAuth2授权类型(Flows)
授权类型选型参考
OAuth2 授权服务器和资源服务器案例实操
授权服务器的组成
Spring Security OAuth2 架构图
code1. 授权码模式授权服务器
code
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- for OAuth 2.0 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
目录结构
资源api
@Controller
public class UserController {
// 资源API
@RequestMapping("/api/userinfo")
public ResponseEntity<UserInfo> getUserInfo() {
// 获取用户信息
User user = (User) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
String email = user.getUsername() + "@spring2go.com";
UserInfo userInfo = new UserInfo();
userInfo.setName(user.getUsername());
userInfo.setEmail(email);
return ResponseEntity.ok(userInfo);
}
}
授权服务器配置
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// inMemory 授权服务器的相关信息存在内存中, 不适用生产级
clients.inMemory()
// clientapp 客户名称, 112233 客户密码 -> 客户凭证, 一般通过授权服务器注册, 此demo简化
.withClient("clientapp")
.secret("112233")
// 重定向url, 拿到授权码后跳转回客户端
.redirectUris("http://localhost:9001/callback")
// 指定授权码模式
.authorizedGrantTypes("authorization_code")
// 细分权限
.scopes("read_userinfo", "read_contacts");
}
}
资源服务器配置
@Configuration
// 启动资源服务器
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
// 对哪些资源 (api端点) 下面的调用 进行 OAuth2的认证
// UserController的资源必须带着token过来, 如果不带token or token失效 就会被拒绝
.antMatchers("/api/**");
}
}
lab
1. 发送请求获取授权码
http://localhost:8080/oauth/authorize?
client_id=clientapp
&redirect_uri=http://localhost:9001/callback // 重定向的URI
&response_type=code // 表示通过授权码的方式走流程
&scope=read_userinfo // 只读userinfo
跳转到该url
http://localhost:9001/callback?code=4bupW0 // 拿到授权码
2. 获取访问令牌
使用 postman 模拟 post请求 到 授权码端点
http://localhost:8080/oauth/token
http://localhost:8080/oauth/token?
code=y8h8sP // 第一步拿到的授权码
&grant_type=authorization_code
&redirect_uri=http://localhost:9001/callback
&scope =read_userinfo
3. 访问资源
code2. 简化模式授权服务器
code
除了授权服务器配置, 其余都相同
授权服务器配置
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("clientapp")
.secret("112233")
.redirectUris("http://localhost:9001/callback")
// 简化模式
.authorizedGrantTypes("implicit")
// 访问令牌的有效期, 120 seconds
.accessTokenValiditySeconds(120)
.scopes("read_userinfo", "read_contacts");
}
}
lab
1.获取访问令牌
http://localhost:8080/oauth/authorize?
client_id=clientapp
&redirect_uri=http://localhost:9001/callback
&response_type=token // 与授权码模式(code)不同
&scope=read_userinfo
&state=abc // 可选, 安全起见生产环境启用
返回
http://localhost:9001/callback#
access_token=c15889f3-5a3f-4346-ac42-1afe77ec5414 // 在url中返回了token
&token_type=bearer
&state=abc
&expires_in=119
2.调用API访问资源
code3. 密码模式授权服务器
code
除了授权服务器配置, 其余都相同
// 授权服务器配置
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 用户认证, 配置在授权服务器端点上 AuthorizationServerEndpoints
// 因为用户名-密码模式, 直接把用户名密码传递给授权服务器,
// 授权服务器拿到用户名密码需要通过 SpringSecurity 的 AuthenticationManager 进行认证
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// inMemory模式, 授权服务器的相关信息存在内存中
clients.inMemory()
// 用户名, 密码 -> 用户凭证
.withClient("clientapp")
.secret("112233")
// 密码模式
.authorizedGrantTypes("password")
// 细分权限
.scopes("read_userinfo", "read_contacts");
}
}
lab
1. 获取访问令牌
使用 postman 模拟 post 请求
2. 请求资源服务器
code4. 客户端模式授权服务器
code
资源api作修改
@Controller
public class UserController {
// 资源API
@RequestMapping("/api/userinfo")
public ResponseEntity<UserInfo> getUserInfo() {
// 获取用户信息
User user = (User) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
String email = user.getUsername() + "@spring2go.com";
UserInfo userInfo = new UserInfo();
userInfo.setName(user.getUsername());
userInfo.setEmail(email);
return ResponseEntity.ok(userInfo);
}
}
授权服务器
// 授权服务器配置
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("clientdevops")
.secret("789")
// 客户端模式
.authorizedGrantTypes("client_credentials")
// 细分权限
.scopes("devops");
}
}
lab
1. 获取访问令牌
2. 访问资源
JWT令牌
类型分类
JWT是一种 自包含令牌
自解释的token 需要有签名 signature
类似支票
对应audience,
JWT 自解释, 自校验特性
code
配置授权服务器
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
/**
* 用户名密码模式需要启用 spring security 的 AuthenticationManager
*/
@Autowired
private AuthenticationManager authenticationManager;
/**
* 对JWT进行签名的话需要增加一个秘钥, 在授权服务器也要通过这个秘钥来解签
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("test-secret");
return converter;
}
/**
* 生成的token是由JwtTokenStore生成的
* @return
*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* 配置授权端点, 支持生成JWT的token, 签名需要用到 accessTokenConverter
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore())
.accessTokenConverter(accessTokenConverter());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 设置客户凭证
.withClient("clientapp")
.secret("112233")
.scopes("read_userinfo")
// 授权服务器可以同时支持三种模式
.authorizedGrantTypes(
"password",
"authorization_code",
"refresh_token");
}
}
配置资源服务器
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends
ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated().and()
// 对api的访问端点都需要进行校验
.requestMatchers().antMatchers("/api/**");
}
}
在application.properties里设置校验的私钥key
server.port=8081
#springboot 自动的去加载相应的jwt处理的代码
#如果请求的是jwt的token, 用该配置去作为解签签名去校验令牌 是否是正确令牌
security.oauth2.resource.jwt.key-value=test-secret
lab
1.获取JWT令牌
使用 postman 模拟 POST 请求
获得令牌
2.请求资源服务器
OpenID Connect 简介
OAuth2 – 授权协议
在OAuth2上增加了一个身份认证层
下一代微服务安全架构
方案一
③ 做一个access token 和 JWT token 的一个交换
因为 access token 是透明的, 无意义的
而且不想让后面的微服务分别校验, 让网关集中校验
每个微服务可以根据自己的情况增加OAuthFilter
因为JWT token是自校验的, 是无状态的校验方式, 不需要到授权服务器去集中校验
可以通过OAuthFilter去校验JWT token并获取相应的用户信息
方案二
授权服务器和API网关之间有一个秘钥
好处: 网关不需要到授权服务器上去集中校验, 可以本地自校验
坏处: 缺少了集中校验环节, 没有集中的吊销点, 只能等JWT自动过期
没办法一次性把用户剔除, 建议JWT自动过期时间设置得短一点
方案三
网关做 access token 和 JWT 转换时使用 redis 缓存, 第一种方案的优化
监控指标
高可用的, 多台的, 水平扩容