分布式系统认证方案
需要满足以下需求
- 统一认证授权:不同客户端均采用一致的认证、授权、会话判断机制,实现统一认证授权服务。
- 多样的认证场景:支持用户名密码、短信验证码、二维码、人脸识别等各种认证方式,并可以灵活的切换和扩展。
- 应用接入认证:,提供安全的系统对接机制,并可开放部分
API
给第三方使用。并且内部服务和外部第三方服务均采用统一的接入机制。
分布式认证方案
基于Session的认证方式
session复制
将服务器A的session
,复制到服务器B,同样将服务器B的session
也复制到服务器A,这样两台服务器的session
就一致了。像tomcat
等web
容器都支持session
复制的功能,在同一个局域网内,一台服务器的session
会广播给其他服务器。
缺点:同一个网段内服务器太多,每个服务器都会去复制session
,会造成服务器内存浪费。
session黏性
利用Nginx
服务器的反向代理,将服务器A和服务器B进行代理,然后采用ip_hash
的负载策略,将客户端和服务器进行绑定,也就是说客户端A第一次访问的是服务器B,那么第二次访问也必然是服务器B,这样就不存在session
不一致的问题了。
缺点:如果服务器A宕机了,那么客户端A和客户端B的session
就会出现丢失。
session集中管理
这种方式就是将所有服务器的session
进行统一管理,可以使用v
等高性能服务器来集中管理session
,而且spring
官方提供的spirng-session
就是这样处理session
的一致性问题。这也是目前企业开发用到的比较多的一种分布式session
解决方案。
基于Token的认证方式
基于token
的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token
存在任意地方,并且可以实现web
和app
统一认证机制。token
认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0
、JWT
等。一般情况服务端无需存储会话信息,减轻了服务端的压力。
缺点
:token
由于自包含信息,因此一般数据量较大,而且每次请求
都需要传递,因此比较占带宽。另外,token
的签名验签操作也会给cpu
带来额外的处理负担。
验证流程
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.0
是OAuth
协议的延续版本,但不向后兼容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
可以被签名,例如,用公钥/私钥对,可以验证内容没有被篡改。
Header
:token
的类型(JWT
)和算法名称(比如:HMAC SHA256
或者RSA
等等)Payload
:JWT
的第二部分是payload
,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:registered
,public
和private
。Signature
:签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token
,它还可以验证JWT的发送方是否为它所称的发送方。
注意,不要在
JWT
的payload
或header
中放置敏感信息,除非它们是加密的
授权码模式
举例:小米账户授权Authorization Code
授权分为两步,首先获取Authorization Code
,然后用Code
换取Access Token
。其流程示意图如下
一般会有下面的注意事项:
Authorization Code
只能使用一次,不可重复使用Authorization Code
有效时间为5分钟, 在返回Authorization Code
5分钟之后失效
简化模式
简化模式相对于授权码模式,少了获取code
以及用code
换token
这一步,用户授权后,认证服务器直接返回一个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 过滤器
两个至关重要的类:OncePerRequestFilter
和GenericFilterBean
,在过滤器链的过滤器中,或多或少间接或直接继承到
OncePerRequestFilter
顾名思义,能够确保在一次请求只通过一次filter
,而不需要重复执行。GenericFilterBean
是javax.servlet.Filter
接口的一个基本的实现类GenericFilterBean
将web.xml
中filter
标签中的配置参数-init-param
项作为bean
的属性GenericFilterBean
可以简单地成为任何类型的filter
的父类GenericFilterBean
的子类可以自定义一些自己需要的属性GenericFilterBean
,将实际的过滤工作留给他的子类来完成,这就导致了他的子类不得不实现doFilter
方法GenericFilterBean
不依赖于Spring
的ApplicationContext
,Filters
通常不会直接读取他们的容器信息(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
又称跨域请求伪造,攻击方通过伪造用户请求访问受信任站点。- 对需要验证的请求验证是否包含
csrf
的token
信息,如果不包含,则报错。这样攻击网站无法获取到token
信息,则跨域提交的信息都无法通过过滤器的校验。
LogoutFilter
- 匹配
URL
,默认为/logout
- 匹配成功后则用户退出,清除认证信息
RequestCacheAwareFilter
- 通过
HttpSessionRequestCache
内部维护了一个RequestCache
,用于缓存HttpServletRequest
SecurityContextHolderAwareRequestFilter
- 针对
ServletRequest
进行了一次包装,使得request
具有更加丰富的API
AnonymousAuthenticationFilter
- 当
SecurityContextHolder
中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder
中。匿名身份过滤器,这个过滤器很重要,需要将它与UsernamePasswordAuthenticationFilter
放在一起比较理解,spring security
为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。 - 匿名认证过滤器是
Spirng Security
为了整体逻辑的统一性,即使是未通过认证的用户,也给予了一个匿名身份。而AnonymousAuthenticationFilter
该过滤器的位置也是非常的科学的,它位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilter
、BasicAuthenticationFilter
、RememberMeAuthenticationFilter
)之后,意味着只有在上述身份过滤器执行完毕后,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
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
private TokenStore tokenStore;
private ClientDetailsService clientDetailsService;
private AuthorizationCodeServices authorizationCodeServices;
private AuthenticationManager authenticationManager;
//令牌访问端点
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//注意这个部分,到时候和请求链接的参数比对
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"); //验证回调地址
}
//令牌管理服务
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;
}
// 令牌访问端点安全策略
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()") //oauth2/token_key
.checkTokenAccess("permitAll()") //oauth/check_key
.allowFormAuthenticationForClients(); // 表单认证 (申请令牌)
}
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 | /** |
1 | @Configuration |
访问http://localhost:8000/oauth/authorize?client_id=pa&reponse_type=code&scope=all&redirect_uri=http://www.baidu.com
确认授权:
获取到授权码:
[参考]