分布式系统认证方案

分布式系统认证方案

需要满足以下需求

  • 统一认证授权:不同客户端均采用一致的认证、授权、会话判断机制,实现统一认证授权服务。
  • 多样的认证场景:支持用户名密码、短信验证码、二维码、人脸识别等各种认证方式,并可以灵活的切换和扩展。
  • 应用接入认证:,提供安全的系统对接机制,并可开放部分API给第三方使用。并且内部服务和外部第三方服务均采用统一的接入机制。

分布式认证方案

基于Session的认证方式

session复制

将服务器A的session,复制到服务器B,同样将服务器B的session也复制到服务器A,这样两台服务器的session就一致了。像tomcatweb容器都支持session复制的功能,在同一个局域网内,一台服务器的session会广播给其他服务器。

缺点:同一个网段内服务器太多,每个服务器都会去复制session,会造成服务器内存浪费。

image

session黏性

利用Nginx服务器的反向代理,将服务器A和服务器B进行代理,然后采用ip_hash的负载策略,将客户端和服务器进行绑定,也就是说客户端A第一次访问的是服务器B,那么第二次访问也必然是服务器B,这样就不存在session不一致的问题了。

缺点:如果服务器A宕机了,那么客户端A和客户端B的session就会出现丢失。
image

session集中管理

这种方式就是将所有服务器的session进行统一管理,可以使用v等高性能服务器来集中管理session,而且spring官方提供的spirng-session就是这样处理session的一致性问题。这也是目前企业开发用到的比较多的一种分布式session解决方案。

基于Token的认证方式

基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token存在任意地方,并且可以实现webapp统一认证机制。
token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0JWT等。一般情况服务端无需存储会话信息,减轻了服务端的压力。

缺点token由于自包含信息,因此一般数据量较大,而且每次请求
都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

image

验证流程

image
1 用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(oauth-service)中认证。

2 认证服务调用验证该用户的身份是否合法,并获取用户权限信息。

3 认证服务获取接入方权限信息,并验证接入方是否合法。

4 若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权限。

5 后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。

6 API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。

7 如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。

8 微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事:

  • 用户授权拦截(看当前用户是否有权访问该资源)
  • 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时
    获取当前用户信息)

OAuth2.0

OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。OAuth2.0OAuth协议的延续版本,但不向后兼容OAuth1.0即完全废止了OAuth1.0

OAuth2中的四种认证授权模式

  • 授权码模式(authorization code
  • 简化模式/隐式授权模式(implicit
  • 密码模式(password
  • 客户端模式(client credentials

OAuth2.0包括以下角色

  • 客户端:本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
  • 资源拥有者:通常为用户,也可以是应用程序,即该资源的拥有者。
  • 授权服务器(认证服务器):用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。
  • 资源服务器: 存储资源的服务器。
JWT token

JWT的全称是JSON Web Tokens,它由下面2部分组成:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : JWT可以被签名,例如,用公钥/私钥对,可以验证内容没有被篡改。

image

  • Headertoken的类型(JWT)和算法名称(比如:HMAC SHA256或者RSA等等)
  • PayloadJWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, publicprivate
  • Signature:签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

注意,不要在JWTpayloadheader中放置敏感信息,除非它们是加密的

授权码模式

举例:小米账户授权
image
Authorization Code授权分为两步,首先获取Authorization Code,然后用Code换取Access Token。其流程示意图如下
image

一般会有下面的注意事项:

  • Authorization Code只能使用一次,不可重复使用
  • Authorization Code有效时间为5分钟, 在返回Authorization Code5分钟之后失效
简化模式

简化模式相对于授权码模式,少了获取code以及用codetoken这一步,用户授权后,认证服务器直接返回一个token

密码模式

这个模式流程简单,但很不安全,一般用在强信任的两个系统。

客户端模式

分配一个账号密码,每一个账户密码资源服务器会对允许请求的资源做权限控制。可以用这个账号密码来获取token,通过这个token去获取资源服务器允许你请求的资源,之所以使用token而不直接使用账户密码还是为了安全考虑,token有过期机制,过期后需要使用账户密码重新获取token,很多时候这个重新获取的业务场景被称为签到。

认证服务器会给准入的接入方一个身份,用于接入时的凭据:
client_id:客户端标识,client_secret:客户端秘钥
因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。

Spring-Security-OAuth2

Spring-Security-OAuth2是对OAuth2的一种实现,OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。

SpringSecurity 过滤器

两个至关重要的类:OncePerRequestFilterGenericFilterBean,在过滤器链的过滤器中,或多或少间接或直接继承到

  • OncePerRequestFilter顾名思义,能够确保在一次请求只通过一次filter,而不需要重复执行。
  • GenericFilterBeanjavax.servlet.Filter接口的一个基本的实现类

    • GenericFilterBeanweb.xmlfilter标签中的配置参数-init-param项作为bean的属性
    • GenericFilterBean可以简单地成为任何类型的filter的父类
    • GenericFilterBean的子类可以自定义一些自己需要的属性
    • GenericFilterBean,将实际的过滤工作留给他的子类来完成,这就导致了他的子类不得不实现doFilter方法
    • GenericFilterBean不依赖于SpringApplicationContextFilters通常不会直接读取他们的容器信息(ApplicationContext concept)而是通过访问spring容器(Spring root application context)中的service beans来获取,通常是通过调用filter里面的getServletContext() 方法来获取

      WebAsyncManagerIntegrationFilter

  • 根据请求封装获取WebAsyncManager

  • WebAsyncManager获取/注册SecurityContextCallableProcessingInterceptor

SecurityContextPersistenceFilter

  • 先实例SecurityContextHolder->HttpSessionSecurityContextRepository(下面以repo代替).作用:其会从Session中取出已认证用户的信息,提高效率,避免每一次请求都要查询用户认证信息。
  • 根据请求和响应构建HttpRequestResponseHolder
  • repo根据HttpRequestResponseHolder加载context获取SecurityContext
  • SecurityContextHolder将获得到的SecurityContext设置到Context中,然后继续向下执行其他过滤器
  • finally-> SecurityContextHolder获取SecurityContext,然后清除,并将其和请求信息保存到repo,从请求中移除FILTER_APPLIED属性

HeaderWriterFilter

  • 往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制

CsrfFilter

  • csrf又称跨域请求伪造,攻击方通过伪造用户请求访问受信任站点。
  • 对需要验证的请求验证是否包含csrftoken信息,如果不包含,则报错。这样攻击网站无法获取到token信息,则跨域提交的信息都无法通过过滤器的校验。

LogoutFilter

  • 匹配URL,默认为/logout
  • 匹配成功后则用户退出,清除认证信息

RequestCacheAwareFilter

  • 通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

SecurityContextHolderAwareRequestFilter

  • 针对ServletRequest进行了一次包装,使得request具有更加丰富的API

AnonymousAuthenticationFilter

  • SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。匿名身份过滤器,这个过滤器很重要,需要将它与UsernamePasswordAuthenticationFilter 放在一起比较理解,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
  • 匿名认证过滤器是Spirng Security为了整体逻辑的统一性,即使是未通过认证的用户,也给予了一个匿名身份。而AnonymousAuthenticationFilter该过滤器的位置也是非常的科学的,它位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilterBasicAuthenticationFilterRememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext依旧没有用户信息,AnonymousAuthenticationFilte该过滤器才会有意义—-基于用户一个匿名身份。

SessionManagementFilter

  • securityContextRepository限制同一用户开启多个会话的数量
  • SessionAuthenticationStrategy防止session-fixation protection attack(保护非匿名用户)

ExceptionTranslationFilter

  • ExceptionTranslationFilter异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
  • 此过滤器的作用是处理中FilterSecurityInterceptor抛出的异常,然后将请求重定向到对应页面,或返回对应的响应错误代码

FilterSecurityInterceptor

  • 获取到所配置资源访问的授权信息
  • 根据SecurityContextHolder中存储的用户信息来决定其是否有权限
  • 主要一些实现功能在其父类AbstractSecurityInterceptor

UsernamePasswordAuthenticationFilter

  • 表单认证是最常用的一个认证方式,一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录,而这背后的UsernamePasswordAuthenticationFilter,在整个Spring Security的认证体系中则扮演着至关重要的角色

使用

pom.xml

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

授权服务器配置:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;

//令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//注意这个部分,到时候和请求链接的参数比对
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //用内存存储
.withClient("c1")
.secret(new BCryptPasswordEncoder().encode("secret")) //客户端密钥
.resourceIds("res1") //资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "refresh_token")
.scopes("all")//允许授权范围
.autoApprove(false)
.redirectUris("http://www.baidu.com"); //验证回调地址
}

//令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService); //客户端服务信息
service.setSupportRefreshToken(true); // 是否产生刷新令牌
service.setTokenStore(tokenStore); //令牌存储策略
service.setAccessTokenValiditySeconds(7200);// 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
return service;
}

// 令牌访问端点安全策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()") //oauth2/token_key
.checkTokenAccess("permitAll()") //oauth/check_key
.allowFormAuthenticationForClients(); // 表单认证 (申请令牌)
}

@Bean
public AuthorizationCodeServices authorizationCodeServices() {
//设置授权码模式的授权码如何 存取,暂时采用内存方式
return new InMemoryAuthorizationCodeServices();
}
}

如果要用数据库就在这个UserDetailsService编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class CustomUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// 1. 查询用户 数据库查出角色权限


// 2. 设置角色
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority;

if ("admin".equals(login)) {
grantedAuthority = new SimpleGrantedAuthority("ADMIN");
} else {
grantedAuthority = new SimpleGrantedAuthority("USER");
}
grantedAuthorities.add(grantedAuthority);
//写死 用户密码123以及角色 USER 查出来的密码和比对的加密算法要传入进去
return new User(login,
new BCryptPasswordEncoder().encode("123"), grantedAuthorities);
}
}

1
2
3
4
5
6
7
8
9
10
11
/**
* 在config包下定义TokenConfig,
* 我们暂时先使用InMemoryTokenStore,生成一个普通的令牌。
*/
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
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
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//认证管理器
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
;

}
}

访问http://localhost:8000/oauth/authorize?client_id=pa&reponse_type=code&scope=all&redirect_uri=http://www.baidu.com
image
确认授权:
image
获取到授权码:
image

[参考]

循序渐进之单点登录(4)–分布式系统认证(OAuth2,JWT)

Spring Security 核心过滤器链分析