[TOC]
说明:本文《分布式认证中心实现方案》是本人的一些拙见,会存在我未想到的不足之处,还请大佬指出。
源码地址:https://github.com/sunwebgo/distributed-authentication-center
参考:https://blog.csdn.net/zlbdmm/article/details/118692985
https://blog.csdn.net/qq_35427589/article/details/127340635
【黑马程序员Java进阶教程快速入门Spring Security OAuth2.0认证授权】 https://www.bilibili.com/video/BV1VE411h7aL/?share_source=copy_web&vd_source=0b39c0c0ea3977b251975ea88134799d
1.实现方案
本实现方案整合Spring Security
和OAuth2.0
开放标准,采用的是用户名和密码模式。token
的存储策略是Redis
,在网关处对token
进行校验和用户授权,实现refresh_token
无感知刷新token
。
系统模块如下:
模块 |
说明 |
mc-gateway |
网关模块 |
mc-auth |
认证中心 |
mc-dynamic |
动态服务(资源服务) |
流程图如下:
2.OAuth2
2.1OAuth简介
OAuth
(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务。
下边分析一个Oauth2认证的例子,通过例子去理解OAuth2.0协议的认证流程,本例子是网站使用微信认证的过程,这个过程的简要描述如下:
- 客户端请求第三方授权用户进入程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
- 资源拥有者同意给客户端授权:资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站。
- 客户端获取到授权码,请求认证服务器申请令牌:客户端应用程序请求认证服务器,请求中携带授权码。
- 认证服务器向客户端响应令牌:微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。
- 客户端携带令牌访问资源服务器的资源:网站携带令牌请求访问微信服务器获取用户的基本信息。
- 资源服务器返回受保护资源:资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
OAauth2.0包括以下角色:
- 客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
- 资源拥有者
通常为用户,也可以是应用程序,即该资源的拥有者。
- 授权服务器(也称认证服务器)
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
- 资源服务器
存储资源的服务器,本例子为微信存储的用户信息。
现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:
- client_id:客户端标识
- client_secret:客户端秘钥
因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。
2.2OAuth2的四种授权模式
OAuth 2.0 定义了四种授权方式,每种方式适用于不同的场景和需求。
2.2.1授权码(authorization code)
这是最常用且安全性最高的授权方式。适用于有后端的 Web 应用。流程如下:
- 用户点击 A 网站提供的链接,跳转到 B 网站并授权用户数据给 A 网站。
- B 网站返回一个授权码给 A 网站。
- A 网站使用授权码在后端向 B 网站请求令牌。
- 资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:
参数列表如下:
client_id
:客户端准入标识。
response_type
:授权码模式固定为code。
scope
:客户端权限。
redirect_uri
:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
- 浏览器出现向授权服务器授权页面,之后同意授权。
- 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
- 客户端拿着授权码向授权服务器索要访问access_token,请求如下:
- 授权服务器返回令牌(access_token)
2.2.2隐藏式(implicit)
适用于纯前端应用,没有后端的情况。令牌直接传给前端,但安全性较低,令牌有效期通常只在会话期间内。
- 用户跳转到 B 网站,登录并同意授权。
- B 网站将令牌作为 URL 锚点传给 A 网站。
2.2.3密码式(password)
用户直接将用户名和密码告知应用,应用使用这些凭据申请令牌。
- A 网站要求用户提供 B 网站的用户名和密码。
- A 网站使用这些凭据向 B 网站请求令牌。
参数列表如下:
client_id
:客户端准入标识。
client_secret
:客户端秘钥。
grant_type
:授权类型,填写password表示密码模式
username
:资源拥有者用户名。
password
:资源拥有者密码。
2.2.4客户端凭证(client credentials)
适用于客户端应用,不涉及用户的授权。
- 第三方应用先备案,获取客户端 ID 和客户端密钥。
- 应用使用这些凭证直接向授权服务器请求令牌。
3.认证中心基础搭建
3.1pom依赖
认证中心的pom依赖如下:
3.2application.yaml配置文件
3.3创建oauth信息实体类
oauth信息实体类用于读取application.yaml文件当中的oauth信息:
3.4实现UserDetailsService接口
因为采用的是用户名-密码
模式,所以需要实现spring security
实现的UserDetailsService
接口去查询数据库验证用户名和密码:
LoginUser
实现UserDetails
接口,封装用户对应的角色编号,用于后续用户的权限校验:
3.5Security配置
SecurityConfig
配置类的作用是注入BCryptPasswordEncoder
(密码采用BCrypt
的加密方式)、认证管理器和放行的请求:
3.6token配置
token的存储策略就采用redis,创建token配置类TokenConfig
,注入RedisTokenStore
,设置token存储前缀:
3.7token增强配置
通过实现TokenEnhancer
接口,实现enhance
方法来对token进行增强。添加字段用户信息实体UserInfoVO
和用户角色roles
,用于后续的用户鉴权:
3.8配置OAuth授权服务配置
3.8.1授权服务配置类介绍
可以用 @EnableAuthorizationServer
注解并继承AuthorizationServerConfigurerAdapter
来配置OAuth2.0 授权服务器。
AuthorizationServerConfigurerAdapter
要求配置以下几个类,这几个类是由Spring
创建的独立的配置对象,它们会被Spring
传入AuthorizationServerConfigurer
中进行配置。
AuthorizationServerConfigurerAdapter
类如下:
授权服务配置结构:
重写AuthorizationServerConfigurerAdapter
类当中的三个方法,每个方法的作用如下:
ClientDetailsServiceConfigurer
:用来配置客户端详情服务。
AuthorizationServerEndpointsConfigurer
:用来配置令牌(token)的访问端点和令牌服务(token services)。
AuthorizationServerSecurityConfigurer
:用来配置令牌端点的安全约束.
3.8.2客户端详情、令牌访问端点和令牌访问端点安全约束配置
3.8.2.1客户端详情配置
ClientDetailsServiceConfigurer 能够使用内存或者JDBC来实现客户端详情服务(这里采用内存的方式), 客户端详情服务(ClientDetailsService)负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:
clientId
:(必须的)用来标识客户的Id。
secret
:(需要值得信任的客户端)客户端安全码,如果有的话。
scope
:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes
:此客户端可以使用的授权类型,默认为空。
authorities
:此客户端可以使用的权限(基于Spring Security authorities)。
客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。
3.8.2.2令牌访问端点配置
AuthorizationServerEndpointsConfigurer
是 Spring Security OAuth2 中的一个配置类,用于配置 OAuth2 授权服务器的端点(endpoints)。通过设置以下属性,我们可以决定支持的授权类型(Grant Types):
authenticationManager
:指定用于验证用户身份的 AuthenticationManager
实例。这是必需的,因为授权服务器需要验证用户的凭据。
tokenStore
:指定用于存储访问令牌的 TokenStore
实现类。不同的 TokenStore
实现方式决定了令牌的存储位置,如内存、数据库或 Redis,TokenStore
的实现类如下:
- InMemoryTokenStore:将 OAuth2 访问令牌保存在内存中,使用
ConcurrentHashMap
管理。这是一种简单且轻量级的实现方式
- JdbcTokenStore:将 OAuth2 访问令牌存储在数据库中,通常使用关系型数据库(如 MySQL、PostgreSQL)来持久化令牌数据。这样可以实现跨服务器共享令牌信。
- JwkTokenStore:用于处理 JSON Web Key Set(JWKS)中的令牌。JWKS 是一种用于安全传输令牌的标准格式,通常与 OpenID Connect 和 OAuth2 配合使用。
- RedisTokenStore:将 OAuth2 访问令牌存储在 Redis 数据库中,具有高性能和可扩展性。这对于分布式系统和微服务架构非常有用
**userDetailsService
**:指定用于加载用户信息的 UserDetailsService
实现类。授权服务器需要根据用户名查找用户信息,以便生成令牌。
**authorizationCodeServices
**:指定用于处理授权码授权类型的服务。授权码授权类型通常用于 Web 应用程序的身份验证流程。
**implicitGrantService
**:指定用于处理隐式授权类型的服务。隐式授权类型通常用于单页应用程序(SPA)的身份验证流程。
**tokenGranter
**:指定自定义的 TokenGranter
实现类,用于支持自定义的授权类型。例如,你可以实现自己的授权类型,然后在这里注册。
AuthorizationServerEndpointsConfigurer
允许我们根据项目需求配置授权服务器的不同端点,以支持不同的授权类型。
配置授权端点的URL(Endpoint URLs):
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping()
的方法用来配置端点URL链接,它有两个参数:
- 第一个参数:String 类型的,这个端点URL的默认链接。
- 第二个参数:String 类型的,你要进行替代的URL链接。
以上的参数都将以 “/“ 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的 第一个参数:
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源服务访问的令牌解析端点。
- /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
那么就通过pathMapping()
方法将获取token的接口/oauth/token
映射到/login
,即用户登录的url。
3.8.2.3令牌访问端点安全配置
AuthorizationServerSecurityConfigure:用来配置令牌端点(Token Endpoint)的安全约束,在 AuthorizationServer中配置如下。
3.8.3授权服务配置类具体实现
授权服务配置类AuthorizationServerConfig
具体实现:
自此,认证中心的授权服务配置已经搭建完成,测试通过/login
接口请求token:
授权类型为password
:
请求头中添加客户端信息:
请求头中的格式为client_id:client-secret
的Base64编码格式
请求接口,获取token,查看redis当中存储的token信息:
3.9切面类自定义响应
由于/login
(/oauth/token
)的响应格式不是统一响应格式code,data,message格式,所以,通过切面类来自定义/login
接口响应。
首先查看源码TokenEndpoint
类,/oauth/token
请求映射方法就是postAccessToken
创建切面类:
采用环绕通知,在执行目标方法postAccessToken()
之前,添加 grant_type 和 scope 参数(**密码模式**),grant_type
为password
,scope
是all
。
在执行目标方法postAccessToken()
获取到token之后,重新定义响应体,改为统一响应体格式。
再次测试获取token:
4.网关环境搭建
网关在这里的主要作用是不仅仅是断言、过滤并路由到指定服务,还需要在网关处对token进行校验、刷新token已经对用户身份进行校验。
4.1pom依赖
网关依赖
4.2application.yaml配置文件
4.3白名单配置类
4.4跨域配置
4.5RestTemplate配置类
4.6/oauth/token原始响应实体类
4.7token认证异常处理类
4.8网关全局过滤器
Spring Cloud Gateway
本质上就是一个过滤器链,通过实现gateway
提供的GlobalFilter
来实现网关过滤器,并通过Order
来设置该网关过滤器的优先级,其优先级最高,在网关过滤器链最前面。
4.9refresh_token无感刷新token
在生产环境中,refresh_token
的过期时间要比token
长的多。当token
过期时,可以通过refresh_token
来获取新的token
。grant_type
为refresh_token
模式。
在本系统中refresh_token
实现无感刷新的方案是:
- 通过oauth提供的校验token的接口
/oauth/check_token?token=******
来验证token是否错误或者是失效。
- 如果验证不通过,则由
RefreshTokenHandle
类来处理
RefreshTokenHandle
类:
但是这个类本身就是为了放行请求到下游服务,refresh_token
的操作则由切面类RefreshTokenAspect
:
切面类RefreshTokenAspect
采用的是环绕通知,在目标方法filter()
执行之前,发送HTTP请求通过refresh_token
来获取新的token
。如果刷新token成功,则通过joinPoint.proceed()
执行目标方法(执行filter(),放行请求到下游服务),而如果刷新失败(refresh_token
因过期或者是错误无效),则不放行请求到下游服务,实现网关处拦截,通过oAuthExceptionHandler.writeError
来提示用户重新登录。
4.10token无感刷新测试
- 首先进行登录
- 查看redis当中的token信息
- 等待token过期
- 通过过期的token和未过期的refresh_token发送请求测试
- 查看redis当中的token信息
可以看到已经实现了token的无感刷新。
5.资源服务配置
5.1资源服务配置说明
资源服务要做的就是创建拦截器(过滤器也可以),拦截所有请求,判断请求头当中是否存在refresh_info
:
refresh_info
就是在网关处通过refresh_token
生成的新的token
,并将refresh_token
和token
放入到请求头中,传递给下游服务。
所以在下游服务(资源服务)需要创建拦截器(或者过滤器)拦截所有请求,判断请求头当中是否存在refresh_info
,如果有就证明token
无效,并通过refresh_token
生成了新的token
,然后将生成的新的refresh_token
和新的token
设置到响应头中,然后在前端就能够获取到并设置到localStorage
。
5.2OAuthRefreshTokenInterceptor拦截器
5.3配置拦截器
6.前端操作
6.1登录存储token和refresh_token
成功登录后将token和refresh_token存储到localStorage
中。
6.2拦截器
- 请求拦截器的作用是拦截除忽略外的所有请求,向请求头中添加
Authorization
字段和refresh_token
字段。
- 响应拦截器的作用是拦截所有响应,判断响应头中是否存在新的token和refresh_token,如果存在就存储到
localStorage
中