上面流程图描述了用户要操作的各个微服务,用户查看个人信息需要访问用户微服务,下单需要访问订单微服务,秒杀抢购商品需要访问秒杀微服务。
每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色然后授权访问对应的功能。
用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。说通俗点,就相当于校验用户账号密码是否正确
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访,这个过程叫用户授权
用户访问的项目中,至少有3个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的 定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设 计概念了。 所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的 登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如 Facebook,Twitter等,国内的比如微博、微信、QQ等。
Java中有很多用户认证的框架都可以实现单点登录:
1、Apache Shiro.
2、CAS
3、Spring security
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵 循一定的 接口协议。 OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时, 任何第三方都可以使用OAUTH认 证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而 OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发 包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如 Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放 资源授权的标准。 Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
Oauth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容, Oauth2.0是 Oauth协议的延续版。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Oauth2有一下授权模式:
Oauth2包括以下角色:
1、客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购、Android客户端、畅购Web客户端(浏览器端)、微信客户端等。
2、资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。
3、授权服务器(也称认证服务器) 用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要 想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。
4、资源服务器 存储资源的服务器,比如,畅购用户管理服务器存储了畅购的用户信息,微信的资源服 务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。
授权码授权流程:
1、客户端(比如说淘宝官网)请求第三方授权
2、资源拥有者(用户)同意给客户端授权
3、客户端获取到授权码,请求认证服务器申请令牌
4、认证服务器向客户端响应令牌
5、客户端请求資源服务器的资源,资源服务校验令牌合法性,完成授权
6、资源服务器返回受保护资源
授权码模式授权步骤流程:
Get请求:http://localhsot:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
首先跳转到登录页面:http://localhost:9001/login
输入账号和密码,点击Login。
Spring Security接收到请求会调用UserDetailsService接口的 loadUserByUsername方法查询用户正确的密码。 当前导入的基础工程中客户端ID为changgou,秘钥 也为changgou即可认证通过。
下面页面代表密码正确,认证成功。(因为没有配置登陆成功后的页面才会Error)。
接下来进入授权页面:http://localhost:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=k45iLY就是返回的授 权码, 每一个授权码只能使用一次。
下图是已经返回授权码,在路径中显示http://localhost/?code=qWk1Th
。(无法访问此网站是因为我们在申请授权码的时候路径中写的重定向的页面除了问题)
拿到授权码后,申请令牌。需要用POST请求:http://localhost:9001/oauth/token
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 http Basic认证。 什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用 base64编 码,放在header中请求服务端,一个例子:
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户 名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。
==客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。 ==
使用BCryptPasswordEncoder().encode();
方式加密
需要在注入
/***
* 采用BCryptPasswordEncoder对密码进行编码
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
建表语句:
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密算法加密',
`scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
点击发送: 申请令牌成功
返回结果:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MjAzNzYwNDQ4OCwianRpIjoiNTcxNDM1NmMtYWU4MC00NjNmLWI5MzktNGFlMTA1YWZjOThkIiwiY2xpZW50X2lkIjoiY2hhbmdnb3UiLCJ1c2VybmFtZSI6ImNoYW5nZ291In0.POGOk6Tm1ioqRmY2q10dNp-dl5mMwu4OR9XBd4xVadUcYYWaYELf2GnYyJYuqzYMvzRKToumn3659toZIbz9eh-XRFHlYtJ-vdS5h5v8CudoNKidMemdB1O7PgtTUt1UZNbE3TJNf_MUAP3evGJ9FyHZYDogkcFDPRUj-PEG7hK07p8OSczhEtjaIX94PpzXQCOm30MXD9bJ487sjbjoYnhzmXl8KnODn30M65rN-rUGVkn-qpIrGwhzgmtcKktjC2D-8vVX9nuHMLwdcG_8lujcIC49XI-l_IBz3DH3XdSLv4cotB-XZ022W5ZHEPsLdLfDyeOcyIxU5isAHIlEqg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwiYXRpIjoiNTcxNDM1NmMtYWU4MC00NjNmLWI5MzktNGFlMTA1YWZjOThkIiwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MjAzNzYwNDQ4OCwianRpIjoiZDk2NmVjYmUtNzZhMi00NjVjLWEyZWQtMDZmNDk1OGJhM2M3IiwiY2xpZW50X2lkIjoiY2hhbmdnb3UiLCJ1c2VybmFtZSI6ImNoYW5nZ291In0.Xitu9p64rRuDJJWcfKBtCkMvVzlIOghUIJaChU-JHOtD4a0jCOMfHA2gcvfDVu_u9z-n1pVFhvk9V3qGhO1FeOb3DJKwz1mdEAaQj-jyvmYFerJ1ZFcxWzqSrYkYcn6nFAaA7ABTShQjgXXb4UdWN0vvXGKqDSX4TMcIFb6qrT6Y4cAeaN9dqc2DvbQRfXUYq5Oy36HQH5jOp1JKTfHda8dofNvLaG-ohvVkBaMVOgP2QZi_K1x1aUy2Ti2f4ou8NxYAsf0B49XqmfaVx0KEzlFuEGRjB1jAHF9seaA3dh2_Fv8rMMERk3DDBvqvRmeLRCKuTIeof-AIgxdVX_euIw",
"expires_in": 431999999,
"scope": "app",
"jti": "5714356c-ae80-463f-b939-4ae105afc98d"
}
Spring Security Oauth2提供校验令牌的端点:GET请求
http://localhost:9001/oauth/check_token?token= [access_token]
替换申请令牌时返回的access_token
。
http://localhost:9001/oauth/check_token?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MjAzNzYwNDQ4OCwianRpIjoiNTcxNDM1NmMtYWU4MC00NjNmLWI5MzktNGFlMTA1YWZjOThkIiwiY2xpZW50X2lkIjoiY2hhbmdnb3UiLCJ1c2VybmFtZSI6ImNoYW5nZ291In0.POGOk6Tm1ioqRmY2q10dNp-dl5mMwu4OR9XBd4xVadUcYYWaYELf2GnYyJYuqzYMvzRKToumn3659toZIbz9eh-XRFHlYtJ-vdS5h5v8CudoNKidMemdB1O7PgtTUt1UZNbE3TJNf_MUAP3evGJ9FyHZYDogkcFDPRUj-PEG7hK07p8OSczhEtjaIX94PpzXQCOm30MXD9bJ487sjbjoYnhzmXl8KnODn30M65rN-rUGVkn-qpIrGwhzgmtcKktjC2D-8vVX9nuHMLwdcG_8lujcIC49XI-l_IBz3DH3XdSLv4cotB-XZ022W5ZHEPsLdLfDyeOcyIxU5isAHIlEqg
POST请求:http://localhost:9100/oauth/token
表单格式
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。
Post请求: http://localhost:9001/oauth/token
携带参数:
grant_type : 密码模式授权填写 password
username: 账号
password: 密码
并且此链接需要使用 http Basic认证。
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:
Spring Security提供对WT的支持,本节我们使用 Spring Security提供的 HwtHelper来创建Jwt令牌,校验WT令牌等操作。这里Jwt令牌我们采用非对称算法进行加密,所以我们要先生成公钥和私钥
(1)生成密钥证书下边命令生成密钥证书,采用RSA算法每个证书包含公钥和私钥。
创建一个文件夹jks
,在该文件夹下执行如下命令行
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
Keytool 是一个java提供的证书管理工具:
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,changgou.jks保存了生成的证书
-storepass:密钥库的访问密码
keytool -list -keystore changgou.jks
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
使用brew下载安装openssl
brew install openssl
Mac终端一直卡在Updating Homebrew…解决方法
在官网下载:下载openssl
使用tar
命令解压,查看版本openssl version
进入jks
文件夹下,使用以下命令
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoc0Zs7om5mT5K9nZ6Bo9
goH6NXevuJ0twXG2pysE0i4JpzjhmSHRGN1TbgA4qZTvO4887J96YXvBTsVjdH3Q
iF5niSECxe0ThjSeSCfgOUegliLSwMTYiqQDXXA7LfLPaGxto+3ba+bSf2BpnunH
85xAp/ZsSm89yEqa7ib+ReT/fKZNbtNZuqlQeW9oFhw02axcd0m8gxXvePjN84E/
YQLIt2YswVYZ2N5isjVxzGbVaTqxryGZjJPj6T26L3KtkDF1QE9XPw9EMBwlVhql
WPV+pb/3wYyLfp5kObFcDD3MICoaOiB3gBiQr1tCxvGSfh7qjKe7oXwyRsIyAtVU
GQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgIELLsXbzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJj
bjEMMAoGA1UECBMDbHNoMQwwCgYDVQQHEwNsc2gxDDAKBgNVBAoTA2xzaDEMMAoG
A1UECxMDbHNoMQwwCgYDVQQDEwNsc2gwHhcNMjAxMTEzMDk0OTU4WhcNMjEwMjEx
MDk0OTU4WjBTMQswCQYDVQQGEwJjbjEMMAoGA1UECBMDbHNoMQwwCgYDVQQHEwNs
c2gxDDAKBgNVBAoTA2xzaDEMMAoGA1UECxMDbHNoMQwwCgYDVQQDEwNsc2gwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChzRmzuibmZPkr2dnoGj2Cgfo1
d6+4nS3BcbanKwTSLgmnOOGZIdEY3VNuADiplO87jzzsn3phe8FOxWN0fdCIXmeJ
IQLF7ROGNJ5IJ+A5R6CWItLAxNiKpANdcDst8s9obG2j7dtr5tJ/YGme6cfznECn
9mxKbz3ISpruJv5F5P98pk1u01m6qVB5b2gWHDTZrFx3SbyDFe94+M3zgT9hAsi3
ZizBVhnY3mKyNXHMZtVpOrGvIZmMk+PpPbovcq2QMXVAT1c/D0QwHCVWGqVY9X6l
v/fBjIt+nmQ5sVwMPcwgKho6IHeAGJCvW0LG8ZJ+HuqMp7uhfDJGwjIC1VQZAgMB
AAGjITAfMB0GA1UdDgQWBBQnC9d8sSfqijSEreqLWRd5kFy3+jANBgkqhkiG9w0B
AQsFAAOCAQEAMyJilsJ5+hYoyqjEA15j1FQU59xJOkjXqBJm2PeZsAR/ksm7jQIO
/HdpB2QukCA2Knx+KukENigKZhp9TlG+jjUgUH8TF02rULR3/gZT5VLVnl9eY+E2
Zl/w17ML+aIkd+xy0bz+JqhRvK4pQoaKdLC+4w/UaTVW57/VgvY1PtdtulrH9qzO
MlV1bYjVIqtmEz9Uu3eXjFM0EOfjleSeVFTkX/OVo7c/jfKDEwsv/WmO8Ig75B6S
sx+fk3UjTDUdJhF1nK/nkTJmw/F7Wzy2QT+QIVM7p8ReBcOk5ScrqI9kqp/qYUkI
5ZKh7Ri3Upuq9ibZCFX7LT4vPsFGNJ4gRg==
-----END CERTIFICATE-----
将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoc0Zs7om5mT5K9nZ6Bo9goH6NXevuJ0twXG2pysE0i4JpzjhmSHRGN1TbgA4qZTvO4887J96YXvBTsVjdH3QiF5niSECxe0ThjSeSCfgOUegliLSwMTYiqQDXXA7LfLPaGxto+3ba+bSf2BpnunH85xAp/ZsSm89yEqa7ib+ReT/fKZNbtNZuqlQeW9oFw02axcd0m8gxXvePjN84E/YQLIt2YswVYZ2N5isjVxzGbVaTqxryGZjJPj6T26L3KtkDF1QE9XPw9EMBwlVhqlWPV+pb/3wYyLfp5kObFcDD3MICoaOiBgBiQr1tCxvGSfh7qjKe7oXwyRsIyAtVUGQIDAQAB-----END PUBLIC KEY-----
注意:
Jwt使用私钥加盐
/*****
* @Author: www.itheima
* @Date: 2019/7/7 13:42
* @Description: com.changgou.token
* 创建JWT令牌,使用私钥加密
****/
public class CreateJwtTest {
/***
* 创建令牌测试
*
* 1.加载证书
* 2.读取证书数据
* 3.获取私钥
* 4.创建令牌、需要私钥加盐[RSA算法 非对称加密]
*/
@Test
public void testCreateToken(){
//证书文件路径
String key_location="changgou.jks";
//秘钥库密码
String key_password="changgou";
//秘钥密码
String keypwd = "changgou";
//秘钥别名
String alias = "changgou";
//访问证书路径 读取jks的文件
ClassPathResource resource = new ClassPathResource(key_location);
//创建秘钥工厂 加载读取证书数据
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());
//读取秘钥对(公钥、私钥) 获取证书中 的一对秘钥 (别名,密码)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());
//获取私钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();
//自定义Payload
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", "1");
tokenMap.put("name", "itheima");
tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
//生成Jwt令牌 JwtHelper是SpringSecurity提供的类 (载荷,盐(私钥 ))
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));
//取出令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
/*****
* @Author: www.itheima
* @Date: 2019/7/7 13:48
* @Description: com.changgou.token
* 使用公钥解密令牌数据
****/
public class ParseJwtTest {
/***
* 校验令牌
*/
@Test
public void testParseToken(){
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.hDDJDNFsX-lrIXJC_k4s01qSlmWb_Pq43G2ZSDrwmf4ugwNC0ViwBztxGIfhJ4K74dZpKj4pqOqo83U_fjUDHT80VsM3sIRuL_nXc43fqSvQUg_WIWqUlX1VYyS413FFn1u_V_9yOnxmQKi1p5bIp644XeGFMSJS3_U3rYLZYQtr6Q8VGbciUaEKimoZQf8ca4kbk1szfyye4llBzixgh3gH4UjKrX-kB-qgo4eDD0dW0UBCxqhpoS-QN4pc3ZtrkndW7xRVrjU0VrIJbNtQmXwk3vE0anOuD1yowh2jY3QmFwR323YcLXPPbGP-KrvPBmJJLC9X2nffCQR61HMJTw";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoc0Zs7om5mT5K9nZ6Bo9goH6NXevuJ0twXG2pysE0i4JpzjhmSHRGN1TbgA4qZTvO4887J96YXvBTsVjdH3QiF5niSECxe0ThjSeSCfgOUegliLSwMTYiqQDXXA7LfLPaGxto+3ba+bSf2BpnunH85xAp/ZsSm89yEqa7ib+ReT/fKZNbtNZuqlQeW9oFhw02axcd0m8gxXvePjN84E/YQLIt2YswVYZ2N5isjVxzGbVaTqxryGZjJPj6T26L3KtkDF1QE9XPw9EMBwlVhqlWPV+pb/3wYyLfp5kObFcDD3MICoaOiB3gBiQr1tCxvGSfh7qjKe7oXwyRsIyAtVUGQIDAQAB-----END PUBLIC KEY-----";
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取Jwt原始内容 载荷
String claims = jwt.getClaims();
System.out.println(claims);
//jwt令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}