Spring Cloud Security与OAuth2

Spring Cloud Security

分布式系统的安全控制

Spring Security安全控制原理:Filter

启动的时候创建一个过滤器的代理,代理将请求转化到Spring定义的一系列Filter,最终实现针对Web层安全的控制。

Spring Cloud能帮助我们解决哪些问题

image-20220420020945812

OAuth2

是一个标准,与编程语言无关;本质也是一个微服务;

  • Open Authentication

  • 一个基于令牌的安全框架,允许用户使用第三方验证服务进行验证

  • 4个组成部分

  • A protected resource,受保护资源

  • A resource owner,资源所有者

  • An application,应用程序

  • OAuth2 authentication server,OAuth2验证服务器(认证服务器)

image-20220420001725768

认证与访问流程

  1. 用户向认证服务器请求登录
  2. 认证返回token给用户
  3. 用户携带token访问受保护资源
  4. 受保护资源拿着token向OAuth2验证合法性
  5. 返回用户所需资源

分布式系统关系图

image-20220420003632704

开发OAuth2认证服务

添加依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<!-- 不仅仅用在spring cloud框架内 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>

启动类加注解

  • @EnableAuthorizationServer

  • 实现供受保护服务将访问的端点:/auth/user,用于获取认证信息

创建配置类

OAuth2Config-哪些应用程序(Client)、凭据?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService;

// 配置app的信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 存在内存中,实际应用中放在关系数据库中
.withClient("eagleeye") // app名称
.secret("thisissecret") // app密码
.authorizedGrantTypes("refresh_token", "password", "client_credentials") // 授权类型(了解
.scopes("webclient", "mobileclient"); // 被处理的业务的范围
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
个人用户凭据、所属角色定义
  • 支持的存储:内存、JDBC、==LDAP服务器==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
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();
}

// 配置用户的信息,授权
@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");
}
}
authentication vs. authorization
  • 认证与授权

测试

以下URL都是自动实现的

注意/auth是因为在配置文件中加上了:

1
2
server:
contextPath: /auth

验证用户

POSThttp://localhost:8901/auth/oauth/token

提供信息

*为必须)

  1. Auth中(Basic Auth类型)(即头部Authorization)

    • 应用名称*

    • 应用密码*

  2. body中(form-data类型)

    • username*
    • password*
    • grant_type 授权类型
    • scope 使用范围
返回json
  • access_token

  • token_type

    • bearer表示不记名
  • refresh_token:

    刷新的token(token的默认有效期为12小时),当token过期时,需要携带此token再次发起验证获取新的token

  • expires_in:有效期,单位:秒

  • scope:指定的使用范围

如:image-20220420010443513

访问自定义端点:/auth/user(这里是定义在启动类中)

/auth为配置文件的上下文

1
2
3
4
5
6
7
@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;
}

GEThttp://localhost:8901/auth/user

提供信息

携带上一步的token,并在Authorization中选择OAuth2.0类型,Access Token选择Available Tokens

image-20220420010741651

开发受保护资源(服务)1(licensing)

添加依赖

1
2
3
4
5
6
7
8
<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>

配置security.oauth2.resource.userInfoUri,指向到认证服务的端点

这里是通过在run.sh中通过-Dsecurity.oauth2.resource.userInfoUri=$AUTHSERVER_URI引入的

AUTHSERVER_URI在docker-compose中,值为:http://authenticationservice:8901/auth/user

启动类加注解

@EnableResourceServer,指定当前服务是一个受保护的资源,强制执行一个过滤器

权限控制

在业务服务中开发而不是OAuth2,是为了解耦

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/v1/organizations/**") // 针对此url必须拥有下面的ADMIN
.hasRole("ADMIN")
.anyRequest()
.authenticated(); // 其他url必须要认证
}
}

传递Authorization(OAuth2访问令牌)

image-20220420012607928

网关(Zuul)的传递

  • Zuul将OAuth2令牌传播到许可证服务

在Zuul的配置文件(在confsvr中)配置:

1
2
3
# 这是一个黑名单,在其中去掉Authorization即可
# 我也不知道默认是啥样,可能是这样?当然可能也有其他参数:zuul.sensitiveHeaders: Cookie,Set-Cookie,Authorization
zuul.sensitiveHeaders: Cookie,Set-Cookie

配置完成后,zuulserver在调用与访问服务时都会自动带上token了

licensing的传递

在调用organization-service的地方做出一些修改,使用OAuth2RestTemplate,调用远程服务(通过Zuul)

此后在licensing发送请求时就会自动带上token了

  • 原本的restTemplate来自于org.springframework.web.client.RestTemplate

    image-20220420013644877

  • 现在修改为org.springframework.security.oauth2.client.OAuth2RestTemplate

    且需要在启动类中手动注入Bean

    1
    2
    3
    4
    5
    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
    OAuth2ProtectedResourceDetails details) {
    return new OAuth2RestTemplate(details, oauth2ClientContext);
    }

    image-20220420013609807

JWT

解决重复向OAuth2认证的问题

JSON Web Token,是IETF提出的开发标准,旨在为OAuth2令牌提供标准结构

优点

  1. JWT令牌编码为Base64,容易传递
  2. 认证服务器作了签名(不会被伪造)
  3. 自包含信息,不需要调用认证服务
  4. 可扩展

使用

添加依赖

认证服务和受保护资源的服务都要加

1
2
3
4
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

创建Bean(如何创建和签名JWT令牌)

authentication服务、licensing服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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;
}


@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 非常重要
// 加了签名的key(key来自于confsvr中的配置文件)属性名:signing.key
// 签名随便写,但要保证与业务端一致
converter.setSigningKey(serviceConfig.getJwtSigningKey());
return converter;
}

}

authentication在其中多加一个可扩展接口的Bean(可选)

1
2
3
4
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}

OAuth2认证服务:配置到OAuth2服务中

authentication服务中添加配置类:

主要是为了将一些bean注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private TokenStore tokenStore;

@Autowired
private DefaultTokenServices tokenServices;

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Autowired
private TokenEnhancer jwtTokenEnhancer; // 可扩展接口

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 可扩展接口
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

endpoints.tokenStore(tokenStore) //JWT
.accessTokenConverter(jwtAccessTokenConverter) //JWT
.tokenEnhancer(tokenEnhancerChain) //JWT
.authenticationManager(authenticationManager)
.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");
}
}

可扩展接口(可选)

依赖:

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
authentication中创建类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);

((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
可扩展接口的解析

zuul的前置过滤器中添加函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private String getOrganizationId(){
String result="";
if (filterUtils.getAuthToken()!=null){

String authToken = filterUtils.getAuthToken().replace("Bearer ","");
try {
Claims claims = Jwts.parser()
.setSigningKey(serviceConfig.getJwtSigningKey().getBytes("UTF-8"))
.parseClaimsJws(authToken).getBody();
result = (String) claims.get("organizationId");
}
catch (Exception e){
e.printStackTrace();
}
}
return result;
}

与之前相比?

  • 受保护服务中(licensing)不用再配置security.oauth2.resource.userInfoUri
  • 认证服务、licensing、organization服务配置服务中都要加上signing.key,且保持一致

在线工具

jwt编解码工具:jwt.io