微服务架构 | 7.1 基于 OAuth2 的安全认证
目录
新人制作,如有错误,欢迎指出,感激不尽!
欢迎关注公众号,会分享一些更日常的东西!
如需转载,请标注出处!
- 前言
- 1. OAuth2 基础知识
- 1.1 安全性的 4 个组成部分
- 1.2 OAuth2 的工作原理
- 1.3 OAuth2 规范的 4 种类型的授权
- 1.4 OAuth2 的优势
- 1.5 OAuth2 核心原理
- 1.6 JSON Web Token
- 2. 建立 OAuth2 服务器
- 2.1 引入 pom.xml 依赖文件
- 2.2 主程序类上添加注解
- 2.3 添加受保护对象的端点
- 2.4 定义哪些应用程序可以使用服务
- 2.4.1 使用 JDBC 存储
- 2.4.2 使用内存储存
- 2.5 为应用程序定义用户 ID、密码和角色
- 2.6 通过发送 POST 请求验证用户
- 3. 使用 OAuth2 建立并保护服务资源
- 3.1 引入 pom.xml 依赖文件
- 3.2 添加 bootstrap.yml 配置文件
- 3.3 在主程序类上添加注解
- 3.4 定义访问控制规则
- 3.4.1 通过验证用户保护服务
- 3.4.2 通过特定角色保护服务
- 4. 在上下游服务中传播 OAuth2 访问令牌
- 4.1 配置服务网关的黑名单
- 4.2 修改上游服务业务代码
- 4.2.1 下游服务
- 4.2.2 在上游服务中公开 OAuth2RestTemplate 类
- 4.2.3 在上游服务中用 OAuth2RestTemplate 来传播 OAuth2 访问令牌
- 最后
前言
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服务原理与实战》
《B站 尚硅谷 SpringCloud 框架开发教程 周阳》
OAuth2 是一个基于令牌的安全验证和授权框架。他允许用户使用第三方验证服务进行验证。 如果用户成功进行了验证, 则会出示一个令牌,该令牌必须与每个请求一起发送。然后,验证服务可以对令牌进行确认;
1. OAuth2 基础知识
1.1 安全性的 4 个组成部分
- 受保护资源:Resource Server,开发人员想要保护的资源(如一个微服务),需要确保只有已通过验证并且具有适当授权的用户才能访问它;
- 资源所有者:Resource Owner,资源所有者定义哪些应用程序可以调用其服务,哪些用户可以访问该服务,以及他们可以使用该服务完成哪些事情。 资源所有者注册的每个应用程序都将获得一个应用程序名称,该应用程序名称与应用程序密钥一起标识应用程序。 应用程序名称和密钥的组合是在验证 OAuth2 令牌时传递的凭据的一部分;
- 应用程序:Client,这是代表用户调用服务的应用程序。毕竟,用户很少直接调用服务 。相反,他们依赖应用程序为他们工作。
- OAuth2 验证服务器:Authorization Server,OAuth2 验证服务器是应用程序和正在使用的服务之间的中间人。 OAuth2 验证服务器允许用户对自己进行验证,而不必将用户凭据传递给由应用程序代表用户调用的每个服务;
1.2 OAuth2 的工作原理
- 第三方客户端向资源所有者(用户)申请认证请求;
- 【关键】用户同意请求,返回一个许可;
- 客户端根据许可向认证服务器申请认证令牌 Token;
- 客户端根据认证令牌向资源服务器申请相关资源;
1.3 OAuth2 规范的 4 种类型的授权
- 密码( password ) ;
- 客户端凭据( client credential ) ;;
- 授权码( authorization code) ;
- 隐式( imp licit );
1.4 OAuth2 的优势
- 允许开发人员轻松与第三方云服务提供商集成,并使用这些服务进行用户验证和授权,而无须不断地将用户的凭据传递给第三方服务;
1.5 OAuth2 核心原理
- 先有一个 OAuth2 认证服务器,用来创建和管理 OAuth2 访问令牌;
- 接着在受保护资源主程序类上添加一个注解:@EnableResourceServer,该注解会强制执行一个过滤器,该过滤器会拦截对服务的所有传入调用,检查传入调用的 HTTP 首部中是否存在 OAuth2 访问令牌,然后调用
security.oauth2.resource.userInfoUri
中定义的回调 URL 告诉客户端与 OAuth2 认证服务器交互,查看令牌是否有效; - 一旦获悉令牌是有效的,@EnableResourceServer 注解也会应用任何访问控制规则,以控制什么人可以访问服务;
1.6 JSON Web Token
- 考虑到篇幅有限,JWT 相关令牌存储将在中讲解;
2. 建立 OAuth2 服务器
- 验证服务将验证用户凭据并颁发令牌;
- 每当用户尝试访问由,如正服务保护的服务时,验证服务将确认 OAuth2 令牌是否已由其颁发并且尚未过期;
2.1 引入 pom.xml 依赖文件
org.springframework.cloud
spring-cloud-security
org.springframework.cloud
spring-cloud-starter-oauth2
2.2 主程序类上添加注解
- @EnableAuthorizationServer:该服务将作为 OAuth2 服务;
- @EnableResourceServer:表示该服务是受保护资源;(该注解在 3.3 详解)
2.3 添加受保护对象的端点
在 controller 包下;
- 该端点将映射到
/auth/user
端点,当受保护的服务调用/auth/user
时,将会确认 OAuth2 访问令牌,并检索发文手背欧虎服务所分配的角色;
/**
* 用户信息校验
* 由受保护服务调用,确认 OAuth2 访问令牌,并检索访问受保护服务的用户所分配的角色
* @param OAuth2Authentication 信息
* @return 用户信息
*/
@RequestMapping(value = { "/user" }, produces = "application/json")
public Map user(OAuth2Authentication user) {
Map userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
2.4 定义哪些应用程序可以使用服务
在 config 包下;
ClientDetailsServiceConfigurer
支持两种类型的储存:内存存储和JDBC存储,如下分点所示:
2.4.1 使用 JDBC 存储
- OAuth2Config 类:
@Configuration
//继承 AuthorizationServerConfigurerAdapter 类
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
//定义哪些客户端将注册到服务
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//JDBC存储:
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT); //设置我们的自定义的sql查找语句
clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT); //设置我们的自定义的sql查找语句
clients.withClientDetails(clientDetailsService); //从 jdbc 查出数据来存储
}
@Override
//使用 Spring 提供的默认验证管理器和用户详细信息服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
- SecurityConstants 类:里面存放上述提到的 SQL 查询语句;
public interface SecurityConstants {
/**
* sys_oauth_client_details 表的字段,不包括client_id、client_secret
*/
String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
/**
*JdbcClientDetailsService 查询语句
*/
String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details";
/**
* 默认的查询语句
*/
String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
/**
* 按条件client_id 查询
*/
String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
}
2.4.2 使用内存储存
@Configuration
//继承 AuthorizationServerConfigurerAdapter 类
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
//定义哪些客户端将注册到服务
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye") //名称
.secret("thisissecret") //密钥
.authorizedGrantTypes("refresh_token", "password", "client_credentials") //授权类型列表
.scopes("webclient", "mobileclient"); //获取访问令牌时可以操作的范围
}
@Override
//使用 Spring 提供的默认验证管理器和用户详细信息服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
2.5 为应用程序定义用户 ID、密码和角色
在 config 包下:
- 可以从内存数据存储、支持 JDBC 的关系数据库或 LDAP 服务器中存储和检索用户信息;
@Configuration
@EnableWebSecurity
//扩展核心 Spring Security 的 WebSecurityConfigurerAdapter
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
//用来处理验证
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//处理返回用户信息
@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
//configure() 方法定义用户、密码与角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("john.carnell").password("password1").roles("USER")
.and()
.withUser("william.woodward").password("password2").roles("USER", "ADMIN");
}
}
- 上述例子中
john.carnell
用户拥有 USER 用户; william.woodward
拥有 ADMIN 用户;
2.6 通过发送 POST 请求验证用户
- 发送:POST
http://localhost:8901/auth/oauth/token
; - 并在 POST 的请求体里带上应用程序名称、密钥、用户 ID 和密码,可以模拟用户获取 OAuth2 令牌;
3. 使用 OAuth2 建立并保护服务资源
- 创建和管理 OAuth2 访问令牌是 OAuth2 服务器的职责;
- 定义哪些用户角色有权执行哪些操作在单个服务级别上的;
3.1 引入 pom.xml 依赖文件
org.springframework.cloud
spring-cloud-security
org.springframework.security.oauth
spring-security-oauth2
3.2 添加 bootstrap.yml 配置文件
security:
oauth2:
resource:
userInfoUri: http://localhost:8901/auth/user
- 这里添加回调 URL,客户端访问受保护服务时,受保护服务将调用
/auth/user
端点,向 OAuth2 服务器检查访问令牌是否生效;
3.3 在主程序类上添加注解
- @EnableResourceServer:表示该服务是受保护资源;
- 该注解会强制执行一个过滤器,该过滤器会拦截对服务的所有传入调用,检查传入调用的 HTTP 首部中是否存在 OAuth2 访问令牌,然后调用
security.oauth2.resource.userInfoUri
中定义的回调 URL 来查看令牌是否有效; - 一旦获悉令牌是有效的,@EnableResourceServer 注解也会应用任何访问控制规则,以控制什么人可以访问服务;
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //断路器
@EnableResourceServer //表示受保护资源
public class Application {
//注入一个过滤器,会拦截对服务的所有传入调用
@Bean
public Filter userContextFilter() {
UserContextFilter userContextFilter = new UserContextFilter();
return userContextFilter;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.4 定义访问控制规则
在 config 包或 security 包下;
- 要定义访问控制规则,需要扩展 ResourceServerConfigurerAdapter 类井覆盖
configure()
方法; - 有多种定义方法,这里给出常见的两种定义示例:
3.4.1 通过验证用户保护服务
- 即:只由已通过身份验证的用户访问;
//必须使用该注解,且需要扩展 ResourceServerConfigurerAdapter 类
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
//访问规则在 configure() 方法中定义,并且通过传入方法的 HttpSecurity 对象配置
@Override
public void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().authenticated();
}
}
anyRequest().authenticated()
表示需要由已通过验证的用户访问;
3.4.2 通过特定角色保护服务
- 限制只有 ADMIN 用户才能调用该服务的 DELETE 方法;
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/v1/xxxservices/**") //运行部开发人员限制对受保护的 URL 和 HTTP DELETE 动词的调用
.hasRole("ADMIN") //允许访问的角色列表
.anyRequest()
.authenticated();
}
}
anyRequest().authenticated()
表示仍需要由已通过验证的用户访问;- 结合本篇《2.5 为应用程序定义用户 ID、密码和角色》的示例,这里使用 john.carnell USER 用户访问资源将被拒绝,而使用 william.woodward ADMIN 用户访问资源将被通过;
4. 在上下游服务中传播 OAuth2 访问令牌
- 用户已经向 OAuth2 服务器进行了验证,调用 EagleEye Web 客户端;
- EagleEye Web 应用程序( OAuth2 服务器)将通过 HTTP 首都 Authorization 添加 OAuth2 访问令牌;
- Zuul 将查找许可证服务端点,然后将调用转发到其中一个许可证服务的服务器;
- 服务网关需要从传入的调用中复制 HTTP 首部 Authorization;
- 受保护服务使用 OAuth2 服务器确认令牌;
4.1 配置服务网关的黑名单
在 Zuul 的 application.yml 的配置文件里;
-
因为在整个验证流程中,我们需要将 HTTP 首部 Authorization 传递上下游进行权限认证;
-
但在默认情况下,Zuul 不会将敏感的 HTTP 首部(如 Cookie、Set-Cokkie 和 Authorization)转发到下游服务;
-
需要配置 Zuul 的黑名单放行 Authorization;
zuul: sensitiveHeaders: Cookie , Set-Cookie
-
上述配置表示拦截 Cookie , Set-Cookie 传递下游,而 Authorization 会放行;
4.2 修改上游服务业务代码
- 业务代码需要保证将 HTTP 首部 Authorization 注入服务的上下游;
4.2.1 下游服务
- 这里的下游服务就是受保护的服务;
- 其构建方法同本篇的《3. 使用 OAuth2 建立并保护服务资源》
4.2.2 在上游服务中公开 OAuth2RestTemplate 类
可以在主程序类上,也可以在主程序所在包及其子包里创建类;
-
使该类可以被自动装配到调用另一个受 OAuth2 保护的服务;
@Bean public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) { return new OAuth2RestTemplate(details, oauth2ClientContext); }
4.2.3 在上游服务中用 OAuth2RestTemplate 来传播 OAuth2 访问令牌
- 自动装配 OAuth2RestTemplate;
@Component
public class OrganizationRestTemplateClient {
//OAuth2RestTemplate 是标准的 RestTemplate 的增强式替代品,可处理 OAuth2 访问令牌
@Autowired
OAuth2RestTemplate restTemplate;
public Organization getOrganization(String organizationId){
//调用组织服务的方式与标准的 RestTemplate 完全相同
ResponseEntity restExchange =
restTemplate.exchange(
"http://zuulserver:5555/api/organization/v1/organizations/{organizationId}",
HttpMethod.GET,
null, Organization.class, organizationId);
return restExchange.getBody();
}
}