传统登录实现方式在应付分布式、微服务场景时存在的问题:
- 1. 每个微服务都要进行登录校验,十分麻烦,我们需要的是单点登录
- 2. 会话保持问题
- 3. 认证方式单一,无法适应各种认证场景(扫码,指纹...),毫无扩展性
- 4. ... ...
UAA服务集成指南
1.认证与授权
1.1 什么是认证、授权、会话
认证(authentication) :用户认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证形式有:账号密码登录,二维码登录,手机短信登录,指纹认证等方式。
授权(authorization): 用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫授权。
会话(session) :会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到登出的过程
1.2 数据安全
typora-copy-images-to: img typora-root-url: img
一、数据安全
为了保证数据安全,需要解决三个问题:机密性、完整性、身份验证(抗抵赖性)
-
机密性:传输内容非明文,即使数据被外界截获,也不能被他人解释或破解。
-
完整性:传输过程中内容不能够被篡改,若信息被篡改或不完整,接收方能够得知。
-
身份验证(抗抵赖性):接收方能够验证数据的实际发送方,确保数据不是被人“冒名顶替”而伪造的。
二、机密性
机密性可通过加密算法保证,加密算法定义了明文、密文之间如何转换,也就是加解密的过程。加密算法分为:对称加密和非对称加密。
2.1 对称加密算法
对称加密指加密和解密使用相同密钥的加密算法。有时又叫传统密码算法,就是加密秘钥能够从解密密钥中推算出来,同时解密密钥也可以从加密密钥中推算出来。而在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信性至关重要。常见的对称加密算法主要有DES,AES,3DES、RC2、RC4和RC5等。
2.2 非对称加密算法
非对称加密算法需要两个密钥:公开密钥(public key)和私有密钥(private key)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密; 如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。常见的非对称加密算法:RSA、DSA、ECC等。
下面例子演示了RSA算法生成公私钥对、公钥加密、私钥解密的过程:
public class RSAEncrypt {
final static Base64.Decoder decoder = Base64.getDecoder();
final static Base64.Encoder encoder = Base64.getEncoder();
/**
* RSA公钥加密
* @param str 加密字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception 加密过程中的异常信息
*/
public static String encrypt( String str, String publicKey ) throws Exception{
//base64编码的公钥
byte[] decoded = decoder.decode(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = encoder.encodeToString(cipher.doFinal(str
.getBytes("UTF-8")));
return outStr;
}
/**
* RSA私钥解密
* @param str 加密字符串
* @param privateKey 私钥
* @return 明文
* @throws Exception 解密过程中的异常信息
*/
public static String decrypt(String str, String privateKey) throws Exception{
//base64解码后的字符串
byte[] inputByte = decoder.decode(str.getBytes("UTF-8"));
//base64编码的私钥
byte[] decoded = decoder.decode(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
String outStr = new String(cipher.doFinal(inputByte));
return outStr;
}
//生成密钥对
public static KeyPair getKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
//可以理解为:加密后的密文长度,实际原文要小些 越大 加密解密越慢
keyGen.initialize(512);
KeyPair keyPair = keyGen.generateKeyPair();
return keyPair;
}
public static Map<Integer, String> genKeyPair() throws Exception {
Map<Integer, String> keyMap = new HashMap<Integer, String>();
//生成公私钥对
KeyPair keyPair = getKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String publicKeyString = encoder.encodeToString(publicKey.getEncoded());
// 得到私钥字符串
String privateKeyString = encoder.encodeToString(privateKey.getEncoded());
// 将公钥和私钥保存到Map
keyMap.put(0,publicKeyString); //0表示公钥
keyMap.put(1,privateKeyString); //1表示私钥
return keyMap;
}
}
/**
* 测试RSA加解密
*/
@Test
public void test1() throws Exception {
Map<Integer, String> keyMap = RSAEncrypt.genKeyPair();
String content = "传智播客";
System.out.println("随机生成的公钥为:" + keyMap.get(0));
System.out.println("随机生成的私钥为:" + keyMap.get(1));
String messageEn = RSAEncrypt.encrypt(content,keyMap.get(0));
System.out.println("加密后的字符串为:" + messageEn);
String messageDe = RSAEncrypt.decrypt(messageEn,keyMap.get(1));
System.out.println("解密后的字符串为:" + messageDe);
}
三、完整性
信息完整性可通过提取并对比消息摘要的方式来实现。消息摘要就是根据一定的运算规则对原始数据进行某种形式的信息提取,通过消息摘要后的消息摘要的长度总是固定的,它也叫做数据指纹,因为它可以唯一的标识一段数据。常见的摘要算法有:sha1、sha256、md5、crc32等。
下面例子演示了SHA256摘要算法:
public class SHA256 {
/**
* 实现SHA256加密
* @param str 加密后的报文
* @return
*/
public static String getSHA256(String str) {
MessageDigest messageDigest;
String encodestr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes("UTF-8"));
encodestr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodestr;
}
private static String byte2Hex(byte[] bytes) {
StringBuffer stringBuffer = new StringBuffer();
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
stringBuffer.append("0");
}
stringBuffer.append(temp);
}
return stringBuffer.toString();
}
}
/**
* 测试SHA256摘要算法
*/
@Test
public void test2() throws Exception {
String content = "传智播客";
System.out.println(content + ": 第一次摘要后的字符串为:" + SHA256.getSHA256(content));
System.out.println(content + ": 第二次摘要后的字符串为:" + SHA256.getSHA256(content));
}
从上面的内容我们可以了解到摘要算法的特性,发送方和接收方可以约定使用相同的摘要算法对原文进行摘要运算,双方得出的消息摘要若一致,说明数据在传输过程中没有被篡改。
四、身份验证
即使我们保证了数据的机密性和完整性,这里面仍然存在一些问题:
-
接收方若要验证消息完整性,必须得到发送方对消息产生的摘要,若第三方得知摘要算法,那摘要也是可以被伪造的,因此摘要本身也需要被加密。
-
消息发送来源如何确定,怎么确定不是第三方伪造的?
发送方将消息原文使用摘要算法生成摘要,再用私钥对摘要进行加密,生成数字签名,然后将内容附上数字签名一起传输。
接收方收到消息后,用发送方的公钥对数字签名进行解密(能解密成功就完成了对发送方的身份验证),得到摘要A,然后再对原文使用摘要算法生成摘要B,比对摘要A和B是否相同,相同则说明内容没有被篡改。
下面例子演示了使用SHA1作为摘要算法,使用RSA作为签名加密算法的签名及验签过程,在RSAEncrypt.java中增加以下代码:
public static final String SIGN_ALGORITHMS = "SHA1WithRSA";
/**
* RSA签名
* @param content 待签名数据
* @param privateKey 私钥
* @param input_charset 编码格式
* @return 签名值
*/
public static String sign(String content, String privateKey, String input_charset) {
try {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(decoder.decode(privateKey));
KeyFactory keyf = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyf.generatePrivate(priPKCS8);
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(priKey);
signature.update(content.getBytes(input_charset));
byte[] signed = signature.sign();
return encoder.encodeToString(signed);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* RSA验签名检查
* @param content 待签名数据
* @param sign 签名值
* @param public_key 公钥
* @param input_charset 编码格式
* @return 布尔值
*/
public static boolean verify(String content, String sign, String public_key, String input_charset) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] encodedKey = decoder.decode(public_key);
PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(pubKey);
signature.update(content.getBytes(input_charset));
boolean bverify = signature.verify(decoder.decode(sign));
return bverify;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
测试方法如下:
/**
* 测试SHA1WithRSA签名、验证签名
*/
@Test
public void test3() throws Exception {
Map<Integer, String> akeyMap = RSAEncrypt.genKeyPair();//a方密钥对
System.out.println("随机生成的a方公钥为:" + akeyMap.get(0));
System.out.println("随机生成的a方私钥为:" + akeyMap.get(1));
String content = "传智播客";
System.out.println("------------a向b发送数据,使用a的私钥生成签名-----------");
String signature = RSAEncrypt.sign(content, akeyMap.get(1), "utf-8");
System.out.println("原文:'" +content+ "'生成签名:" + signature);
System.out.println("----------b接收到a发的数据,使用a的公钥验证签名-----------");
if (RSAEncrypt.verify(content, signature, akeyMap.get(0), "utf-8")) {
System.out.println("验证签名成功:" + signature);
} else {
System.out.println("验证签名失败!");
}
}
总结:
通过加密算法对数据原文的加密、解密,我们能保证数据传输过程中的机密性。
通过数字签名机制,我们既可以保证数据完整性,也可以对数据来源进行身份验证。
针对非对称加密技术的应用,我们通常使用两种方式:
传输过程中的数据加密,我们使用接收方公钥加密,接收方私钥解密,保证数据在传输过程中是密文。
接收方对信息来源的确认,发送方会使用私钥对数据签名,接收方使用发送方公钥进行验签。
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 基础环境搭建
-
拷贝资料文件夹中的wanxinp2p-uaa-service工程到项目目录下并导入,在设置启动参数时请注意该服务占用端口号为53020
-
创建数据库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 前后端集成测试
我们在实现完注册功能时,已经进行过前后端集成测试,那么登录功能的前后端集成测试,由大家自行独立完成。