https://github.com/carnellj/spmia-chapter7
一个安全的应用程序涉及多层保护,包括:
-
确保有正确的用户控制,以便可以确认用户是它们所说的人,并且他们有权执行正在尝试执行的操作
-
保持运行服务的基础设施是打过补丁且最新的,以让漏洞的风险最低
-
实现网络访问控制,让少量已授权的服务器能够访问服务,并使服务只能通过定义良好的端口进行访问
要实现验证和授权控制,我们将使用Spring Cloud Security和Oauth2标准来保护基于Spring的服务。
Oauth2是一个基于令牌的安全框架,允许用户使用第三方验证服务进行验证。如果用户成功进行了验证,则会出示一个令牌,该令牌必须与每个请求一起发送。然后,验证服务可以对令牌进行确认。
Oauth2背后的主要目标是,在调用多个服务来完成用户请求时,用户不需要在处理请求的时候为每个服务都提供自己的凭据信息就能完成验证。
简单说:就是用户在调用某个受保护的服务时,要先从Oauth2服务器验证获取到令牌,拿着这个令牌才能访问到这个受保护的服务
Oauth2将安全性分解为以下4个组成部分:
-
受保护资源:受保护的服务,需要确保只有已通过验证并且具有适当授权的用户才能访问
-
资源所有者:资源所有者定义哪些应用程序可以调用其服务,哪些用户可以访问该服务,以及他们可以使用该服务完成哪些事情。
资源所有者注册的每个应用程序都将获得一个应用程序名称,该应用程序名称与应用程序密钥一起标识应用程序。应用程序名称和密钥。
应用程序名称和密钥的组合是在验证Oauth2令牌时传递的凭据的一部分
-
Oauth2服务器:Oauth2验证服务器是应用程序和正在使用的服务之间的中间人。Oauth2验证服务器允许用户对自己进行验证,而不必将用户凭据传递给由应用程序代表用户调用的每个服务
实现Oauth2密码授权
1、添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
2、注册为Oauth2服务
@SpringBootApplication
@RestController
@EnableResourceServer
@EnableAuthorizationServer // 告诉Spring Cloud,该服务将作为Oauth2服务
public class Application {
@RequestMapping(value = "/user", produces = "application/json")
public Map<String, Object> user(Oauth2Authentication user) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(
user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
验证与授权的区别:
-
验证是用户通过提供凭据来证明他们是谁的行为
-
授权决定是否允许用户做他们想做的事情
3、配置哪些服务需要验证以及创建验证用户
// 定义哪些应用程序可以使用服务
// AuthorizationServerConfigureAdapter是Spring Security的核心部分,它提供了执行关键验证和授权功能的基本机制
@Configuration
public class OAuth2Config extends AuthorzationServerConfigureAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDeatilsService userDetailService;
// 定义了哪些客户端将注册到服务
@Override
public void configure(ClientDetailsServiceConfigure clients) throws Exception {
clients
// ClientDetailsServiceConfigure类支持两种不同类型的存储:内存存储和jdbc存储
.inMemory()
// 注册应用程序名称为eagleeye允许访问oauth2保护的服务
.withClient("eagleeye")
// 密钥,该密钥在eagleeye应用程序调用oauth2服务器以接收oauth2访问令牌是提供
.secret("thisissecret")
// 传入一个以逗号分隔的授权类型列表,这些授权类型将由Oauth2服务支持;这里设置支持密码授权和客户端凭据授权类型
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
// 定义调用应用程序在请求Oauth2服务器获取访问令牌时可以操作的范围
// 通过定义作用域,可以编写特定于客户端应用程序所工作的作用域的授权规则
// webclient表示web应用程序,mobileclient表示手机程序
// 通过使用作用域,可以在受保护的服务中定义授权规则,该规则可以根据登录的应用程序限制客户端应用程序可以执行的操作。这与用户拥有的权限无关。
// 例如,我们可能希望根据用户是使用公司网络中的浏览器,还是使用移动设备上的应用程序进行浏览,来限制用户可以看到哪些数据
.scopes("webclient", "mobileclient");
}
// 定义了AuthenticationServerConfigure中使用的不同组件
@Override
public void configure(AuthorizationServerEndpointsConfigure endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
// 为应用程序定义用户ID、密码和角色
// 扩展核心Spring Security的WebSecurityConfigurerAdapter
// 需要为Oauth2服务器提供一种验证用户的机制,并返回正在验证的用户的用户信息。这通过在Spring WebSecurityConfigurerAdapter实现中定义authenticationManagerBean()和userDetailsServiceBean()两个bean来完成
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
// AuthenticationManagerBean被Spring Security用来处理验证
@Override
@Bean
public AuthenticationManager authenticationMangerBean() throws Exception {
return super.authenticationManagerBean();
}
// Spring Security使用UserDetailsService处理返回的用户信息,这些用户信息将由Spring Security返回
@Override
@Bean
public UserDetailsService userDetailsServiceBean throws Exception {
return super.userDetailsServiceBean();
}
// 定义用户、密码和角色的地方
// 定义了john.carnell为普通用户,william.woodward为admin用户
@Override
protected void configure(AuthenticationMangerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("john.carnell")
.password("password1")
.roles("USER")
.and()
.withUser("william.woodward")
.password("password2")
.roles("USER", "ADMIN");
}
}
4、验证用户
访问:http://localhost:8901/auth/oauth/token
一旦配置了应用程序名称和密钥,就需要在服务中传递以下信息作为HTTP表单参数:
-
grant_type:正在执行的Oauth2授权类型。例如密码password授权
-
scope:应用程序作用域。例如webclient和mobileclient
-
username:用户登陆的名称
-
password:用户登陆的密码
Oauth2标准期望传递给令牌生成端点的所有参数都是HTTP表单参数。
/auth/oauth/token端点调用将返回json数据:
-
access_token:Oauth2令牌,它将随用户对受保护资源的每个服务调用一起出示
-
token_type:令牌类型。Oauth2规范允许定义多个令牌类型,最常用的令牌类型是不记名令牌(bearer token)
-
refresh_token:包含一个可以提交回Oauth2服务器的令牌,以便在访问令牌过期后重新颁发一个访问令牌
-
expires_in:这是Oauth2访问令牌过期前的描述。在Spring中,授权令牌过期的默认值是12h
-
scope:此Oauth2令牌的有效作用域
有了有效的Oauth2访问令牌,就可以使用验证服务中创建的/auth/user端点来检索与令牌相关联的用户信息了。所有受保护资源都将调用验证服务的/auth/user端点来确认令牌并检索用户信息。
在任何时候调用Oauth2保护的端点(包括Oauth2的/auth/user端点),都需要传递Oauth2访问令牌。为此,要始终创建一个名为Authorization的HTTP首部,并附有Bearer xxxx的值。
使用Oauth2保护服务
虽然创建和管理Oauth2访问令牌是Oauth2服务器的职责,但在Spring中,定义哪些用户角色有权执行哪些操作是在单个服务级别上发生的。
要创建受保护资源,需要执行以下操作:
-
将相应的Spring Security和Oauth2 jar添加到要保护的服务中
-
配置服务以指向Oauth2验证服务
-
定义谁可以访问服务
1、依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifctId>spring-security-oauth2</artifactId>
</dependency>
2、配置服务指向Oauth2验证服务
记住,一旦将组织服务创建为受保护资源,每次调用服务时,调用者必须将包含Oauth2访问令牌的Authentication HTTP首部包含到服务中。然后,受保护资源必须调用该Oauth2服务来查看令牌是否有效。
security:
oauth2:
resource:
userInfoUri: http://localhost:8901/auth/user // 定义回调URL
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
// 告诉微服务,它是一个受保护资源
// 注解强制执行一个过滤器,该过滤器会拦截对该服务的所有传入调用,检查传入调用的HTTP首部中是否存在Oauth2访问令牌
// 然后调用security.oauth2.resource.userInfoUri中定义的回调URL来查看令牌是否有效
// 一旦获悉令牌是有效的,注解也会应用任何访问控制规则,以控制什么人可以访问服务
@EnableResourceServer
public class Application {
@Bean
public Filter userContextFilter() {
return new UserContextFilter();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、定义谁可以访问
访问规则的范围可以从极其粗粒度(任何已通过验证的用户都可以访问整个服务)到非常细粒度(只有具有此角色的应用程序,才允许通过DELETE方法访问此URL)。
1、只有已通过验证的用户才能访问服务URL
// 这个类必须使用@Configuration注解进行标记
// 需要扩展ResourceServerConfigurerAdapter
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated(); // 限制仅限已通过身份验证的用户才能访问
}
}
2、只有具有特定角色的用户才能访问服务URL
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/v1/organizations/**") // 允许开发人员限制对受保护URL和HTTP DELETE调用
.hasRole("ADMIN") // 允许访问的角色列表,该列表由逗号分隔
.anyRequest()
.authenticated();
}
}
传播Oauth2访问令牌
实现这些流程需要做两件事情。第一件事情是需要修改Zuul服务网关,以将Oauth2令牌传播到许可证服务。在默认情况下,Zuul不会将敏感的HTTP首部(如Cookie、Set-Cookie和Authorization)转发到下游服务。
zuul:
sensitiveHeaders: Cookie, Set-Cookie
这一配置是黑名单,它包含zuul不会传播到下游服务的敏感首部。在上述黑名单中没有Authorization值就意味着zuul将允许它通过。如果根本没有设置zuul.sensitiveHeaders属性,zuul将自动组织3个值(Cookie、Set-Cookie和Authorization)被传播。
zuul可以自动传播下游的Oauth2访问令牌,并通过使用@EnableOauth2Sso注解来针对Oauth2服务的传入请求进行授权。
Spring Oauth2提供了一个支持Oauth2调用的新REST模板类Oauth2RestTempate。要使用Oauth2RestTemplate类,需要先将它公开为一个可以被自动装配到调用另一个受Oauth2保护的服务的服务的bean。
@Bean
public Oauth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, Oauth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
@Component
public class OrganizationRestTemplateClient {
@Autowired
OAuth2RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class);
public Organization getOrganization(String organizationId) {
logger.debug("In Licensing Service.getOrganization:{}", UserContext.getCorrelationId());
ResponseEntity<Organization> restExchange = restTemplate.exchange(
"http://zuulserer:5555/api/organization/v1/organizations/{organization-id}",
HttpMethod.GET,
null,
Oragnization.class,
organizationId);
return restExchange.getBody();
}
}
JSON Web Token与Oauth2
Oauth2是一个基于令牌的验证框架,但具有讽刺意味的是,它并没有为如何定义其规范中的令牌提供任何标准。为了矫正Oauth2令牌标准的缺陷,一个名为JSON Web Token(JWT)的新标准脱颖而出。
标准的Spring Cloud Security OAuth2配置和基于JWT的OAuth2配置需要不同的配置类。
1、依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
2、创建JWT令牌存储
// 扩展TokenEnhancer,增强令牌
public class JWTTokenEnhancer implements TokenEnhancer {
@Autowired
private OrgUserRepository orgUserRepo;
private String getOrgId(String username) {
UserOrganization orgUser = orgUserRepo.findByUsername(username);
return orgUser.getOrganizationId();
}
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
String orgId = getOrgId(authentication.getName());
additionalInfo.put("organizationId", orgId);
// 所有附加的属性都放在HashMap中,并设置在传入该方法的accessToken变量上
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
// 定义Spring如何管理JWT令牌的创建、签名和翻译
@Configuration
public class JWTTokenStoreConfig {
@Autowired
private ServiceConfig serviceConfig;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
// 用于出示给服务的令牌中读取数据
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
// 在JWT和Oauth2服务器之间充当翻译
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(serviceConfig.getJwtSigningKey()); // 定义将用于签署令牌的签名密钥
return converter;
}
// 扩展JWT令牌
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}
}
// 定义哪些应用程序可以使用服务
// 通过JWTOAuth2Config类将JWT挂钩到验证服务中
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfiguerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// Spring OAuth允许开发人员挂钩多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain中
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints
.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancer) // 挂钩令牌增强器
.accessTokenConverter(jwtAccessTokenConverter) // 这是钩子,用于告诉Spring Security Oauth2使用JWT
.authenticationManager(authenticationManger)
.userDetailsService(userDetailsService);
}
.....
}
访问:
http://localhost:8901/auth/oauth/token
Base64编码后的字符串可以在https://www.jsonwebtoken.io网站解码转换
配置了JWT后,访问获取令牌的路径端点,access_token和refresh_token都是Base64编码的字符串。
3、在微服务中使用JWT
OAuth2RestTemplate类并不传播JWT的令牌。为了确保许可证服务能够做到这一点,需要添加一个自定义的RestTemplate bean来完成这个注入。
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
// 授权令牌添加到http首部
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
return execution.execute(request, body);
}
}
public class Application {
@Bean
@Primary
public RestTemplate getCustomRestTemplate() {
RestTemplate template = new RestTemplate();
List interceptors = template.getInterceptors();
if (interceptors == null) {
template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
} else {
interceptors.add(new UserContextInterceptor());
template.setInterceptors(interceptors);
}
return template;
}
}
每个服务都使用一个自定义的servlet过滤器(UserContextFilter,参考zuul.md)来从HTTP首部解析出验证令牌和关联ID。
从JWT令牌中解析自定义字段
从流经网关的JWT令牌中解码organizationId字段,可以从前置过滤器处理。
1、依赖
<dependency>
<groupId>io.jsonwebtoken<groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2、在前置过滤器中添加解析方法(前置过滤器类为TrackingFilter,参考zuul.md)
private String getOrganizationId() {
String result = "";
if (filterUtils.getAuthToken != null) {
String authToken = filterUtils.getAuthToken().replace("Bearer", ""); // HTTP首部解析出令牌
try {
// 传入用于签署令牌的签名密钥,使用JWTS类解析令牌
Claims claims = Jwts.parser()
.setSigningKey(serviceConfig.getJwtSigningKey().getBytes("UTF-8"))
.parseClaimsJws(authToken)
.getBody();
resutl = (String) claims.get("organizationId"); // 从令牌中提取出organizationId
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
关于微服务安全的总结
在构建用于生产级别的微服务时,应该围绕以下实践构建微服务安全。
-
对所有服务通信使用HTTPS/安全套接字层(Secure Sockets Layer, SSL)
-
所有服务调用都应通过API网关
-
将服务划分到公共API和私有API
-
通过封锁不需要的网络端口来限制微服务的攻击面
将服务划分到公有API和私有API,一般来说,安全是关于构建访问和执行最小权限概念的层。最小权限是用户应该拥有最少的网络访问权限和特权来完成他们的日常工作。为此,开发人员应该通过将服务分离到两个不同的区域(即公共区域和私有区域)来实现最小权限。
公共区域包含由客户端使用的公共API。公共API微服务应该执行面向工作流的小任务。公共API微服务通常是服务聚合器,在多个服务中提取数据并执行任务。
私有区域充当保护核心应用程序功能和数据的壁垒,它应该只通过一个众所周知的端口访问,并且应该被封锁,只接受来自运行私有服务的网络子网的网络流量。除此之外,私有区域应该拥有自己的服务网关和验证服务。公共API服务应该对私有区域验证服务进行验证。所有的应用程序数据至少应该在私有区域的网络子网中,并且只能通过驻留在私有区域的微服务访问。
四种授权方式
密码授权
客户端凭据授权
鉴权码授权
隐式授权
刷新令牌