UAA服务集成指南
1.认证与授权
1.1 什么是认证、授权、会话
老王是一家超市的老板,随着生意越来越好,员工也越来越多,管理起来越来越费劲,经常会出现对不上帐的情况,甚至有时怀疑是不是员工做了手脚?通过朋友介绍,他引入一款进销存软件,精明的他给自己设置为管理员,并且给每位员工新建了账号,每位员工通过各自的账号登入系统。并且,他还给进货员小张分配了"库存管理"模块功能,销售员小李分配了"销售管理"模块功能。通过一段时间经营,他发现管理越来越精细了,工作轻松了,生意也更上一层楼了。
上述例子覆盖了认证、授权、会话的概念:
进货员小张,销售员小李需要通过各自的账号、密码登入系统,这就是认证。
进货员小张,销售员小李的权限不同,进货员小张只能访问老板给分配的“库存管理”功能,这就是授权。
进货员小张,在登入系统后,系统记着了他的身份,他可以连续在系统操作,而不用每一步操作都要输入用户名、密码进行认证,直至退出系统,这就是一个会话。
认证(authentication) :用户认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证形式有:账号密码登录,二维码登录,手机短信登录,指纹认证等方式。
授权(authorization): 用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫授权。
会话(session) :会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到登出的过程。
你还能列举一些身边的例子吗?… …
1.2 数据安全
在认证和授权过程中,我们需要保证数据存储和传输安全,请参考课件中的”数据安全“讲义。
1.3 用户认证机制
用户认证表面上看只是系统校验用户名(身份)、密码(凭证)的过程。但是,为了会话保持,为了提高安全性,为了适应不同系统架构以及各种各样的认证方式(用户名、密码,二维码,短信,三方认证,单点登录…),认证的机制也在不断演进,传统的账号密码登录变成了只是认证方式中的一种。
1.3.1 基于session认证
目前大多数web应用的用户认证机制都是基于session的。用户认证成功后,在服务端生成用户相关的数据保存在session中(当前会话),而发给客户端的sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验。当用户登出或过期时把服务端session销毁,客户端的session_id也就无效了。
而在分布式的环境下,基于session的认证会出现一个问题。当我们第一次访问网站的时候,负载均衡将本地的请求分配到Web服务器A,那么session创建在Web服务器A,第二次访问的时候如果我们不做处理就不能保证还是会落到Web服务器A了。
这个时候,通常的做法有下面几种:
Session复制:多台应用服务器之间同步session,使session保持一致,对外透明。
Session粘滞:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。
Session数据集中存储:将Session存入分布式缓存集群中,所有服务器应用实例统一从分布式缓存集群中存取Session。
总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机制方式基于cookie,在移动应用上无法有效使用,并且无法跨域,保持住会话的做法非常麻烦。
1.3.2 基于Token认证
随着 Restful API、微服务的兴起,基于 Token 的认证现在已经越来越普遍。基于token的用户认证是一种服务端无状态的认证方式,所谓服务端无状态指的token本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带token,因此服务器端无需存放token数据。
当用户认证后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
令牌举例:锦衣卫…
什么是JWT?
我们现在了解了基于token认证的交互机制,但令牌里面究竟是什么内容?什么格式呢?市面上基于token的认证方式大都采用的是JWT(Json Web Token)。
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
JWT令牌结构:
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
- Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC、SHA256或RSA)。
一个例子:
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
- Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比
如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
一个例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
- Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明
签名算法进行签名。
一个例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
下图中包含一个生成的jwt令牌:
JWT令牌的优点:
-
JWT基于json,非常方便解析。
-
可以在令牌中自定义丰富的内容,易扩展。
-
基于token认证这种方式服务端不用存储认证数据,易维护,扩展性强, token 存在 localStorage 可避免 CSRF,并且可以实现web和app统一认证机制。
-
通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
怎么保证令牌的安全?万一我仿造了一个锦衣卫令牌?非对称加密算法需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。公钥是密钥对中公开的部分,私钥则是非公开的部分,通过这种算法得到的密钥对能保证在世界范围内是唯一的 。用公钥加密的数据只有对应的私钥才可以解密。
JWT令牌的缺点:
token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的负担。但是随着硬件的提升和带宽的提高,这些缺点变得越来越微不足道。
1.3.3 OAuth2.0认证
OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不兼容OAuth 1.0即完全废止了OAuth1.0。
我们通过以下例子了解OAuth2.0的场景:
小王是黑马程序员的一名学员,闲暇时登录黑马程序员技术论坛,
小王发现无需注册,通过第三方的QQ就可以登录,非常方便,他就迫不及待的试了一下
果然,只需要在手机端同意授权,即可登录黑马技术论坛。
这种场景在现实生活中很常见吧,你能列举一些例子吗?… …
OAuth2.0正是实现了上面的机制,它的运行流程如下:
(A)小王访问黑马技术论坛,并使用QQ登录,QQ要求小王授权。
(B)小王同意QQ给予黑马技术论坛授权。
(C)黑马技术论坛使用上一步获得的授权,向QQ认证服务器申请令牌(access_token)。
(D)QQ认证服务器对黑马技术论坛进行认证以后,确认无误,同意发放令牌(access_token)。
(E)黑马技术论坛使用令牌,向QQ资源服务器申请获取小王的基本信息。
(F)QQ的资源服务器确认令牌(access_token)无误,同意向黑马技术论坛开放相关资源(用户信息)。
通过上面流程我们了解到OAuth2.0有以下角色:
- **客户端(Client):**第三方应用,本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源。本例中指的是黑马技术论坛
- **资源拥有者(Resource Owner):**通常为用户,也可以是应用程序,即该资源的拥有者。本例指的是小王。
- **认证服务器(Authorization Server 也称授权服务器):**用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。本例指的是QQ用于提供OAuth2.0开放机制,而专门搭建的认证服务器。
- **资源服务器(Resource Server):**服务提供商存储用户生成的资源的服务器。本例指的是QQ用于存放用户数据的服务器。
在咱们P2P项目中,引入OAuth2的主要目的:
-
将来P2P项目考虑到灵活性和可扩展性,也会支持通过第三方授权登录(例如: QQ,微信等),提高客户体验
-
P2P项目采用前后端分离开发架构,前端有PC端(H5)、有移动端(APP)、还有管理端,包括各个微服务,他们彼此之间都是第三方,这些接入端都需要统一做认证管理
在整个基于OAuth2的认证机制中,获取令牌是最关键的一步,OAuth2**一共提供了四种授权(获取令牌)方式:**授权码模式(Authorization Code)、 隐式授权模式(Implicit)、 密码模式(Resource Owner Password Credentials)、 客户端模式(Client Credentials),四种方式均采用不同的执行流程。由于P2P项目目前只使用了密码模式,所以这里暂时只介绍该模式。
我们只需要往认证服务器上发送请求并传递一些参数即可获得令牌:
http://认证服务器/oauth/token?client_id=p2pweb&client_secret=fgsdgrf&grant_type=password
&username=zhangsan&password=123456
参数列表如下:
- client_id:客户端准入标识。服务提供商不可能随便允许一个客户端就接入到它的认证服务器,需要提供客户端标识和秘钥。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写password表示密码模式
- username:资源拥有者用户名。
- password:资源拥有者密码。
这种模式十分简单,但是直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我们自己开发的情况下,即一般用于我们自己开发的第一方原生App或第一方单页面应用。P2P项目中的各个接入方,都是我们自己开发的项目,所以采用密码模式没有问题。
2.P2P项目认证需求分析
上图的接入方对应OAuth2.0的客户端,无论是P2P自身的应用还是第三方接入都通过统一的机制接入平台,
用户对应OAuth2.0的资源拥有者。
流程描述:
(1)用户登录通过接入方(目前指前端)在P2P平台登录,接入方采取OAuth2.0 密码模式请求认证服务(UAA)。
(2)认证服务(UAA)调用统一账号服务去验证该用户,并获取用户权限信息。
(3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法,。
(4)若登录用户以及接入方都合法,生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权限。
(5)后续,接入方携带jwt令牌对P2P平台的微服务资源进行访问。
(6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。
(7)如果接入方的权限没问题,API网关会将请求转发至微服务,并将原请求附加上解析后的明文用户信息jsonToken,微服务用jsonToken来识别本次请求的用户会话。
流程所涉及到统一账号服务、UAA服务、API网关这三个组件,因此下面介绍三个组件的职责。
1.统一账号服务
提供B端用户和C端用户的登录账号、密码、角色、权限、资源等系统级信息的管理,不包含用户业务信息。
2.UAA服务
它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,并连接“统一账号服务”,完成实际的用户认证、授权功能。
3.API网关
作为系统的唯一入口,API网关封装了系统内部架构,为接入方提供定制的API集合。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
网关整合 OAuth2.0 有两种思路,一种是授权服务器采用 jwt, 统一在网关层验证,判断权限等操作;另一种是让资源端处理,网关只做路由转发。
通过前面的流程描述,显然我们使用了第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
3.集成UAA服务
3.1 UAA服务简介
UAA服务是P2P平台中的统一认证中心,集认证和授权功能于一身,采用Spring Security整合OAuth2.0实现**授权服务器(认证服务器)**角色,同时使用JWT令牌技术来存储和传递用户信息。UAA服务中的Spring Security相关配置和代码最为基础设施直接提供给大家,不再讲解。我们重点关注以下两个功能:
(1)UAA服务给接入方提供了用户认证并返回令牌的接口。
(2)UAA服务在认证过程中,它需要调用统一账号服务来完成c端以及b端用户的实际验证。
其中第(1)点,Spring Security OAuth2已经提供了对外的接口,我们无需实现,第(2)点需要我们进行编码衔接,完成UAA调用统一账号服务的过程。
3.2 基础环境搭建
1. 拷贝资料文件夹中的wanxinp2p-uaa-service工程到项目目录下并导入,在设置启动参数时请注意该服务占用端口号为53020
2. 创建数据库p2p_uaa,执行uaa.sql导入oauth_client_details表,用来存储客户端信息,OAuth2.0接入方认证就是通过此表。大家仅需要了解以下字段即可:
- client_id:接入客户端id
- client_secret:接入客户端秘钥
- access_token_validity:访问token的有效期(秒)
- refresh_token_validity:刷新token的有效期(秒)
- authorized_grant_type:该客户端支持的授权类型,authorization_code,password,client_credentials
- scope:作用域,可以将其作为权限来考虑/使用,也可考虑其他用法,非必填
- authorities:指该客户端的权限,必填
-
访问Apollo,新建一个项目uaa-service,关联common-template中的公共namespace:
然后添加或覆盖如下配置:
micro_service.spring-boot-http命名空间:
server.servlet.context-path = /uaa
micro_service.spring-boot-druid命名空间:
spring.datasource.url = jdbc:mysql://localhost:3306/p2p_uaa?useUnicode=true
-
启动测试
4. 认证(登录)功能实现
4.1 需求分析
集成UAA服务后,该功能交互流程如下(请参考注册登录流程图):
1)前端请求统一账户服务获取短信验证码
2)前端校验手机号是否存在,校验验证码是否正确,如果存在则说明已经注册
3)前端发起登录请求,请求UAA认证服务
4)UAA首先校验客户端是否有权限访问系统,如果有权限则会请求统一账户获取账号及权限信息,生成
Token
5)UAA向客户端响应Token,登录成功。
4.2 功能实现
- 在UAA工程的agent包中,新建AccountApiAgent类,作为调用统一用户服务的Feign代理:
@FeignClient(value = "account-service")
public interface AccountApiAgent {
@PostMapping(value = "/account/l/accounts/session")
RestResponse<AccountDTO> login(@RequestBody AccountLoginDTO accountLoginDTO);
}
- 注意检查启动类上的注解
@EnableFeignClients(basePackages = {"cn.itcast.wanxinp2p.uaa.agent"})
- 在UAA工程的domain包中找到IntegrationUserDetailsAuthenticationHandler类,并实现其中的authentication方法:
/**
* 认证处理过程
* @param domain 用户域 ,如b端用户、c端用户等,用户扩展
* @param authenticationType 认证类型,如密码认证,短信认证等,用于扩展
* @param token SpringSecurity的token对象,可提取用户名、密码等用于认证的信息
* @return UnifiedUserDetails 登录成功
*/
public UnifiedUserDetails authentication(String domain, String authenticationType,
UsernamePasswordAuthenticationToken token) {
return null;
}
参数说明:
- domain: 用户域 ,说明了此次登录的是b端用户还是c端用户,由接入方传入
- authenticationType: 说明了此次登录的方式,如密码认证,短信认证等,由接入方传入
- token: SpringSecurity的token对象,里面存放了接入方调用UAA接口进行认证时传入的用户名、密码等需要验证的信息
返回值说明:
- UnifiedUserDetails SpringSecurity对象,用于存放登录成功时返回的信息,比如账号基本信息、权限、资源等。此方法返回该对象给Spring Security OAuth2框架,Spring Security OAuth2框架会根据里面的内容生成jwt令牌,从而使令牌中保存了登录用户的相关数据。
具体实现如下:
public UnifiedUserDetails authentication(String domain, String authenticationType,
UsernamePasswordAuthenticationToken token) {
//1.从客户端取数据
String username=token.getName();
if(StringUtil.isBlank(username)){
throw new BadCredentialsException("账户为空");
}
if(token.getCredentials()==null){
throw new BadCredentialsException("密码为空");
}
String presentedPassword=token.getCredentials().toString();
//2.远程调用统一账户服务,进行账户密码校验
AccountLoginDTO accountLoginDTO=new AccountLoginDTO();
accountLoginDTO.setDomain(domain);
accountLoginDTO.setUsername(username);
accountLoginDTO.setMobile(username);
accountLoginDTO.setPassword(presentedPassword);
AccountApiAgent accountApiAgent=(AccountApiAgent)ApplicationContextHelper
.getBean(AccountApiAgent.class);
RestResponse<AccountDTO> restResponse=accountApiAgent.login(accountLoginDTO);
//3.异常处理
if(restResponse.getCode()!=0){
throw new BadCredentialsException("登录失败");
}
//4.登录成功,把用户数据封装到UnifiedUserDetails对象中
UnifiedUserDetails unifiedUserDetails=new UnifiedUserDetails
(restResponse.getResult().getUsername(),presentedPassword
,AuthorityUtils.createAuthorityList());
unifiedUserDetails.setMobile(restResponse.getResult().getMobile());
return unifiedUserDetails;
}
4.3 功能测试
#### 4.3.1 登录(生成令牌)
功能说明: 用户登录时生成并返回令牌,该令牌用于访问P2P平台内受保护资源。
**访问路径:**POST http://localhost:53020/uaa/oauth/token
请求参数:
- grant_type: 授权类型,可以是authorization_code,implicit,client_credentials,password
- client_id:接入客户端id
- **client_secret:**接入客户端密钥
- **username:**登录用户名
- **password:**登录密码
- **domain:**用户域 ,如b端管理用户、c端受众用户(扩展协议)
- **authenticationType:**认证类型,如密码认证,短信认证,二维码认证等(扩展协议)
access_token:访问令牌,这是我们所需的令牌
token_type:令牌类型,传递令牌是需要在令牌前面增加这个类型作为前缀
refresh_token:刷新令牌,访问令牌到期后,可以通过刷新令牌重新生成访问令牌
expires_in:访问令牌的有效期,单位是秒
code : 状态码,0表示正常
msg:操作结果,success表示成功
jti:身份令牌,主要用来作为一次性token,从而回避重复请求攻击
4.3.2 解析令牌
功能说明: 从访问令牌中解析出原始数据
**访问路径:**POST http://localhost:53020/uaa/oauth/check_token
请求参数:
- token : 访问令牌
响应内容:
{
"code": 0, //响应编码,0代表成功
"msg": "success", //响应描述
"user_name": "admin", //用户名
"mobile": "18611106983", //手机号
"active": true, //状态
"client_authorities": [ //接入客户端权限
"ROLE_TEST",
"ROLE_RESOURCE"
],
"client_id": "p2pweb",//接入客户端id
"exp": 1546937838, //令牌剩余有效期
"user_authorities": {//登入用户的角色及权限(微服务或应用侧使用)
"ROLE1": [
"p1",
"p2"
]
}
....
}
4.3.3 前后端集成测试
我们在实现完注册功能时,已经进行过前后端集成测试,那么登录功能的前后端集成测试,由大家自行独立完成。