前言
网上微服务token流程的案例有很多,但是关于hydra的资料却少之又少,本文主要讲解hydra使用Oauth2.0授权码模式(authorization code)获取令牌token流程,如果你不清楚Oauth2,建议先百度了解一下。
这里简单说一下授权码模式:
OAuth2.0
四种授权中授权码方式是最为复杂,功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"授权服务商"的认证服务器进行互动。
如上图,授权码模式这种场景下的授权,第三方软件可以通过拿到资源拥有者(用户)授权后的授权码,以及注册时的 client_id 和 client_secret 来换回访问令牌 token 的值,这个token其实就相当于我们的核酸码一样,当核酸码过期时就需要刷新核酸码重新做核酸获取,否则门卫是不会让你进入公司大楼的。
一、hydra简单介绍
ORY Hydra是经过强化,经过OpenID认证的OAuth 2.0服务器和OpenID Connect提供商,针对低延迟,高吞吐量和低资源消耗进行了优化。ORY Hydra 不是身份提供者(用户注册,用户登录,密码重置流程),而是通过登录和同意应用程序连接到您现有的身份提供者。以不同的语言实现登录和同意应用程序很容易,并且提供了示例性的同意应用程序(Go,Node)和 SDK。
源码地址:hydra源码下载
以上是官方原话,简单描述下:
hydra是go语言开发,支持高性能,高并发,它与其他实现了OAuth框架的不同之处在于,它实现了 OAuth 和 OpenID Connect 标准,但却不强制你使用hydra自带的用户管理(登录、注销、配置文件管理、注册)、特定模板引擎或预定义的前端,支持我们自定义用户登录和授权流程,只需要对接hydra提供的规范接口即可。
二、hydra安装步骤
官方推荐使用PostgreSQL数据库,出于教程的目的,我们这里使用doker安装数据库,正式环境下不会这么操作。
官方安装教程:Run Ory Hydra in Docker
# 1.创建一个独立的网段,用于容器相互通信
docker network create hydradev
# 2.拉取pg镜像 也可以使用mysql 官方推荐PostgreSQL9.6+、MySQL5.7+和SQLite
docker pull postgres:12.1
# 3.拉取 hydra
docker pull oryd/hydra:v1.10.6
# 4.运行pg数据库,并挂载到本机(然后链接测试一下 帐号:hydradev 密码:1234)
docker run -it -d \
--network hydradev \
--name hydra-dev-pg \
--restart=always \
-e POSTGRES_PASSWORD=1234 \
-e POSTGRES_USER=hydradev \
-p 5432:5432 \
-v /usr/local/postgres/pgdata:/var/lib/postgresql/data \
postgres:12.1
可以使用navicat等工具来测试是否启动成功:
启动成功后建议新建一个模式,用于区分hydra数据库,我这里新建了hydradev
# 5.设置系统机密环境变量,密钥,用于加密数据库
export SECRETS_SYSTEM=hydra-dev-secret123456789
# 6.设置数据库url环境变量, 默认使用public模式
export DSN='postgres://hydradev:1234@hydra-dev-pg:5432/hydradev?sslmode=disable'
# 7.初始化hydra数据库
docker run -it --rm \
--network hydradev \
oryd/hydra:v1.10.6 \
migrate sql --yes $DSN
数据库初始化完成后的表结构 :
# 8.启动Hydra服务
docker run -it -d \
--restart=always \
--name ory-hydra-zsw-dev \
--network hydradev \
-p 4444:4444 \
-p 4445:4445 \
-e SECRETS_SYSTEM=$SECRETS_SYSTEM \
-e DSN=$DSN \
-e URLS_SELF_ISSUER=https://zsw-hydra.com/ \
-e URLS_CONSENT=https://zsw-cloud.com/auth/consent \
-e URLS_LOGIN=https://zsw-cloud.com/auth/login \
-e URLS_LOGOUT=https://zsw-cloud.com/auth/logout \
-e URLS_POST_LOGOUT_REDIRECT=https://zsw-cloud.com/auth/logout/callback \
-e TTL_ACCESS_TOKEN=5m \
-e TTL_REFRESH_TOKEN=10m \
-e TTL_ID_TOKEN=5m \
oryd/hydra:v1.10.6 serve all# 参数说明
公共API端点服务监听的端口号:默认是4444
admin API端点服务监听的端口号,默认是4445URLS_SELF_ISSUER 授权服务器地址
URLS_CONSENT 用户同意授权地址
URLS_LOGIN 用户登录认证地址
URLS_LOGOUT 用户退出登录地址
URLS_POST_LOGOUT_REDIRECT 用户退出登录成功后跳转到的地址TTL_ACCESS_TOKEN 配置刷新令牌有效的时间。默认值为720h。设置为-1可使刷新令牌永不过期。
TTL_REFRESH_TOKEN 配置标识令牌有效的时间。默认为1小时。
TTL_ID_TOKEN 配置标识令牌有效的时间。默认为1小时。
TTL配置过期时间的设置单位 h m s
我这里主要是为了演示效果,所以只设置了5分钟,正式环境不会设置这么短
https://zsw-hydra.com/ : hydra授权服务器地址,安装hydra的机器ip映射路径
https://zsw-cloud.com/: 微服务前端项目域名
https://zsw-cloud.com/auth/ :微服务后台auth认证中心服务请求地址
注意:hydra默认使用https,由于https会有证书问题,所以这里我使用了 nginx + openssl 生成证书 将https转换成http请求,当然如果你有域名的话可以去阿里云或者腾讯云免费申请ssl证书,就不用像苦逼的博主这样使用openssl本地申请证书了。
hydra授权服务nginx配置:
微服务auth认证中心nginx配置:
ip域名映射:
当然,如果想直接使用http也是可以的,只需要在命令最后加上 --dangerous-force-http 相应的url也要改成http。
# 9.最后查看日志是否正常启动
docker logs ory-hydra-zsw-dev
这里推荐使用docker可视化工具Portainer查看,比命令行要方便很多,安装教程网上有很多,这个请自行百度:
启动成功后hydra服务端就搭建成功了,为了我们后面的认证流程能够畅通无阻,这里建议关闭防火墙或开放端口4444和4445
暂时关闭防火墙命令:systemctl stop firewalld
三、客户端相关接口
Hydra服务端搞定后,我们就需要操作客户端了,官方提供了对应的restful API文档
客户端可以这么理解:每个客户端都对应我们正常开发的dev、test、uat、prod环境。
以下代码中出现的JavaBean取自官方Java SDK
1.新增client
接口路径:https://zsw-hydra.com:4445/clients
请求方式:POST
swagger截图:
代码截图:
变量值:
这里需要注意的https请求默认是开启ssl验证的,所以我在ResTemplate配置中设置了绕过ssl验证,不然请求会报错,具体配置参考: RestTemplate绕过SSL证书校验
细心的同学可能会注意到swagger的参数是驼峰式,而我的代码日志中是下划线分割的,这里我用Gson反序列化了一下,因为hydra的接口参数和响应值必须要用下划线接收。
代码实操:
public OAuth2Client createOAuth2ClientCall(OAuth2Client oAuth2Client) {
Gson gson = new Gson();
String hydraAdminCreateOAuth2ClientCallUrl = adminUrl+"/clients";
log.info("============POST================请求路径:{}", hydraAdminCreateOAuth2ClientCallUrl);
String params = gson.toJson(oAuth2Client);
log.info("============================/clients入参:{}", params);
ResponseEntity<String> createAuth2ClientResponse = restTemplateIgnoreSSL.postForEntity(hydraAdminCreateOAuth2ClientCallUrl, params, String.class);
if (!ObjectUtils.isEmpty(createAuth2ClientResponse.getBody())){
OAuth2Client oAuth2ClientResponse = gson.fromJson(createAuth2ClientResponse.getBody(), OAuth2Client.class);
log.info("============================/clients出参:{}", gson.toJson(oAuth2ClientResponse));
return oAuth2ClientResponse;
}
return null;
}
下面是请求参数和响应值:
// 请求参数
{
"client_id": "zsw-cloud-dev",
"client_name": "zsw-cloud-devName",
"client_secret": "507e1d29-1460-4eab-b706-f3b411bc1717",
"client_secret_expires_at": 0,
"frontchannel_logout_session_required": true,
"frontchannel_logout_uri": "https://zsw-cloud.com/auth/logout/callback",
"grant_types": [
"authorization_code",
"refresh_token",
"implicit",
"client_credentials"
],
"post_logout_redirect_uris": [
"https://zsw-cloud.com/auth/logout"
],
"redirect_uris": [
"https://zsw-cloud.com/auth.html"
],
"response_types": [
"code",
"id_token",
"token"
],
"scope": "openid offline offline_access",
"token_endpoint_auth_method": "client_secret_basic",
"userinfo_signed_response_alg": "none"
}
// 响应内容
{
"allowed_cors_origins": [ ],
"audience": [ ],
"client_id": "zsw-cloud-dev",
"client_name": "zsw-cloud-devName",
"client_secret": "507e1d29-1460-4eab-b706-f3b411bc1717",
"client_secret_expires_at": 0,
"client_uri": "",
"created_at": "2022-07-20T11:42:37Z",
"frontchannel_logout_session_required": true,
"frontchannel_logout_uri": "https://zsw-cloud.com/auth/logout/callback",
"grant_types": [
"authorization_code",
"refresh_token",
"implicit",
"client_credentials"
],
"jwks": { },
"logo_uri": "",
"metadata": { },
"owner": "",
"policy_uri": "",
"post_logout_redirect_uris": [
"https://zsw-cloud.com/auth/logout"
],
"redirect_uris": [
"https://zsw-cloud.com/auth.html"
],
"response_types": [
"code",
"id_token",
"token"
],
"scope": "openid offline offline_access",
"subject_type": "public",
"token_endpoint_auth_method": "client_secret_basic",
"tos_uri": "",
"updated_at": "2022-07-20T11:42:36.909294Z",
"userinfo_signed_response_alg": "none"
}
这里简单说明一下几个重要的参数:
client_id:客户端id,全局唯一
client_secret:客户端密钥,会以加密的方式存进数据库
grant_types:授权类型
redirect_uris:code重定向路径(下面授权流程会介绍)
token_endpoint_auth_method:这个是认证密钥传递方式,有两种写法,官方推荐使用client_secret_basic,也可以写成client_secret_post(下面授权流程也会介绍)
创建完成后就可以看到数据库中hydra_client表里已经存在我们新建的client了
2.查询client
2.1 分页查询
接口路径:https://zsw-hydra.com:4445/clients?limit=?&offset=?
请求方式:GET
public List<OAuth2Client> listOAuth2ClientsCall(Long limit, Long offset) {
Gson gson = new Gson();
String hydraAdminQueryClientListUrl = String.format(adminUrl + "/clients" + "?limit=%s&offset=%s",limit,offset);
log.info("============GET================请求路径:{}", hydraAdminQueryClientListUrl);
ResponseEntity<String> auth2ClientListResponse = restTemplateIgnoreSSL.getForEntity(hydraAdminQueryClientListUrl, String.class);
if (!ObjectUtils.isEmpty(auth2ClientListResponse.getBody())){
Type type = new TypeToken<List<OAuth2Client>>() {}.getType();
return gson.fromJson(auth2ClientListResponse.getBody(), type);
}
return Collections.emptyList();
}
2.2 查询指定client
接口路径:https://zsw-hydra.com:4445/clients/{id}
请求方式:GET
public OAuth2Client getOAuth2ClientCall(String clientId) {
Gson gson = new Gson();
String hydraAdminQueryClientLByIdUrl = adminUrl + "/clients/{id}" ;
ResponseEntity<String> auth2ClientResponse = restTemplateIgnoreSSL.getForEntity(hydraAdminQueryClientLByIdUrl, String.class,clientId);
if (!ObjectUtils.isEmpty(auth2ClientResponse.getBody())){
OAuth2Client oAuth2ClientResponse = gson.fromJson(auth2ClientResponse.getBody(), OAuth2Client.class);
log.info("============================/clients/{id}:{}", oAuth2ClientResponse.toString());
return oAuth2ClientResponse;
}
return null;
}
3.修改client
接口路径:https://zsw-hydra.com:4445/clients/{id}
请求方式:PUT
public OAuth2Client updateOAuth2ClientCall(String clientId, OAuth2Client oAuth2Client) {
Gson gson = new Gson();
String hydraAdminUpdateClientLByIdUrl = adminUrl + "/clients/{id}" ;
HttpEntity<String> httpEntity = new HttpEntity<>(gson.toJson(oAuth2Client));
ResponseEntity<String> updateAuth2ClientResponse = restTemplateIgnoreSSL.exchange(hydraAdminUpdateClientLByIdUrl, HttpMethod.PUT,httpEntity, String.class,clientId);
if (!ObjectUtils.isEmpty(updateAuth2ClientResponse.getBody())){
OAuth2Client oAuth2ClientResponse = gson.fromJson(updateAuth2ClientResponse.getBody(), OAuth2Client.class);
log.info("==============PUT==============/clients/{id}:{}", oAuth2ClientResponse.toString());
return oAuth2ClientResponse;
}
return null;
}
4.删除client
接口路径:https://zsw-hydra.com:4445/clients/{id}
请求方式:DELETE
public void deleteOAuth2ClientCall(String clientId) {
String hydraAdminUpdateDeleteLByIdUrl = adminUrl + "/clients/{id}" ;
restTemplateIgnoreSSL.delete(hydraAdminUpdateDeleteLByIdUrl, clientId);
}
当然client相关还有其他接口,这里就不一一介绍了,具体可以参考官方API。client接口相对来说比较简单,都是crud基本功,下面主要介绍下授权流程。
四、认证授权流程及相关接口
先去官方取图:
从上图可以看出,获取授权码一共需要两个流程:Login和Consent
下面我们就开始完整的演示一遍获取授权码的接口流程:
首先调用起点接口获取login_challenge:
参数说明:
client_id:就是我们上面新建的客户端id
response_type:响应值类型
scope:作用范围
state:这个可以随便写
redirect_uri:授权码重定向url
通过官网api介绍可以看出这是一个302重定向请求,调用成功后会携带login_challenge重定向到我们设置的URLS_LOGIN:
https://zsw-cloud.com/auth/login
代码截图:
这里我简单画了一个登录from,并且重定向到这个页面(主要是为了模拟登录场景)
登录页面如下
用户在前端输入用户密码后,点击登录会调用上面截图中的/userLogin接口,在用户登录接口中可以写一些账号密码、ip校验相关的校验,如果校验通过,下一步就是携带用户信息和login_challenge调用acceptLoginRequest接口:
请求方式:PUT
代码实操:
public RequestWasHandledResponse acceptLoginRequestCall(String loginChallenge, AcceptLoginRequest acceptLoginRequest) {
Gson gson = new Gson();
HttpEntity<String> httpEntity = new HttpEntity<>(gson.toJson(acceptLoginRequest));
String hydraAdminLoginAcceptUrl = adminUrl +"/oauth2/auth/requests/login/accept?login_challenge="+loginChallenge;
log.info("============PUT================请求路径:{}", hydraAdminLoginAcceptUrl);
log.info("============PUT================/login/accept请求参数:{}", gson.toJson(acceptLoginRequest));
ResponseEntity<String> hydraAdminLoginAcceptResponse = restTemplateIgnoreSSL.exchange(hydraAdminLoginAcceptUrl, HttpMethod.PUT, httpEntity, String.class);
log.info("============PUT================请求路径出参hydraAdminLoginAcceptResponse:{}", hydraAdminLoginAcceptResponse);
if (!ObjectUtils.isEmpty(hydraAdminLoginAcceptResponse.getBody())){
RequestWasHandledResponse loginAcceptResponse = gson.fromJson(hydraAdminLoginAcceptResponse.getBody(), RequestWasHandledResponse.class);
return loginAcceptResponse;
}
return null;
}
请求参数:
{"subject":"zsw"}
响应值:
{"redirect_to":"https://zsw-hydra.com/oauth2/auth?client_id=zsw-cloud-dev\u0026login_verifier=317781532a8b4c4dabd27c4b533afb5a\u0026redirect_uri=https%3A%2F%2Fzsw-cloud.com%2Fauth.html\u0026response_type=code\u0026scope=openid\u0026state=zsw123456789"}
// 这里域名后面需要拼接上端口,实际响应值如下
https://zsw-hydra.com:4444/oauth2/auth?client_id=zsw-cloud-dev&login_verifier=317781532a8b4c4dabd27c4b533afb5a&redirect_uri=https://zsw-cloud.com/auth.html&response_type=code&scope=openid&state=zsw123456789
转换响应值后,Login认证流程就结束了,下一步就是重定向到响应值接口,进入Consent流程:
该接口也是302请求,会携带consent_challenge重定向到我们设置的URLS_CONSENT:
https://zsw-cloud.com/auth/consent
代码截图:
在/consent接口中我们需要携带和授权范围参数调用acceptConsentRequest接口完成Consent流程:
请求路径:
请求方式:PUT
代码实操:
public RequestWasHandledResponse acceptConsentRequestCall(String consentChallenge, AcceptConsentRequest acceptConsentRequest) {
Gson gson = new Gson();
HttpEntity<String> httpEntity = new HttpEntity<>(gson.toJson(acceptConsentRequest));
String hydraAdminConsentAcceptUrl = adminUrl +"/oauth2/auth/requests/consent/accept?consent_challenge="+consentChallenge;
log.info("============PUT================请求路径:{}", hydraAdminConsentAcceptUrl);
log.info("============PUT================/consent/accept入参:{}", gson.toJson(acceptConsentRequest));
ResponseEntity<String> hydraAdminConsentAcceptResponse = restTemplateIgnoreSSL.exchange(hydraAdminConsentAcceptUrl, HttpMethod.PUT, httpEntity, String.class);
log.info("============PUT================请求路径出参hydraAdminConsentAcceptResponse:{}", hydraAdminConsentAcceptResponse);
if (!ObjectUtils.isEmpty(hydraAdminConsentAcceptResponse.getBody())){
RequestWasHandledResponse consentAcceptResponse = gson.fromJson(hydraAdminConsentAcceptResponse.getBody(), RequestWasHandledResponse.class);
return consentAcceptResponse;
}
return null;
}
请求参数:
// 注意这里如果不加offline,后面的token接口不会返回刷新令牌
{"grant_scope":["openid","offline"]}
响应值:
{"redirect_to":"https://zsw-hydra.com/oauth2/auth?client_id=zsw-cloud-dev\u0026consent_verifier=c61383e8186341ff923f7c338483b30a\u0026redirect_uri=https%3A%2F%2Fzsw-cloud.com%2Fauth.html\u0026response_type=code\u0026scope=openid\u0026state=zsw123456789"}
// 这里域名后面需要拼接上端口,实际响应值如下
https://zsw-hydra.com:4444/oauth2/auth?client_id=zsw-cloud-dev&consent_verifier=c61383e8186341ff923f7c338483b30a&redirect_uri=https://zsw-cloud.com/auth.html&response_type=code&scope=openid&state=zsw123456789
转换响应值后,Consent认证流程也结束了,下一步我们继续重定向到响应值接口:
该接口会携带code(授权码)重定向到我们的redirect_uri地址,然后我们就可以通过授权码获取认证token了
重定向路径如下:
auth.html是我们的授权认证页面,这里我简单的画了一个
具体逻辑代码如下图:
页面效果如下图:
得到授权码code后,下面进入token流程。
五、token相关接口
我们在授权流程结束后拿到code授权码,下一步就是调用后端接口获取token并返回给前端https://zsw-cloud.com/auth/login/callback
接口代码截图:
注意:上面我在新建client时参数token_endpoint_auth_method的区别就在于一种需要将client_secret放到请求体中,一种则是加密后放到请求头中。
下一步调用https://zsw-hydra.com:4444/oauth2/token获取token
代码实操:
public Oauth2TokenResponse oauth2TokenCall(Oauth2TokenRequest tokenRequest, HttpHeaders httpHeaders) {
Gson gson = new Gson();
String hydraPublicTokenUrl = publicUrl + "/oauth2/token";
log.info("============POST================请求路径:{}", hydraPublicTokenUrl);
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.put("grant_type", tokenRequest.getGrantType());
map.put("client_id", tokenRequest.getClientId());
map.put("redirect_uri", tokenRequest.getRedirectUri());
if (!ObjectUtils.isEmpty(tokenRequest.getCode())){
// 获取token
map.put("code", tokenRequest.getCode());
}
if (!ObjectUtils.isEmpty(tokenRequest.getRefreshToken())){
// 刷新token
map.put("refresh_token", tokenRequest.getRefreshToken());
}
log.info("============POST================/oauth2/token入参:{}", gson.toJson(map));
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, httpHeaders);
ResponseEntity<String> tokenResponse = restTemplateIgnoreSSL.exchange(hydraPublicTokenUrl, HttpMethod.POST, httpEntity, String.class);
log.info("============POST================/oauth2/token出参:{}", tokenResponse);
if (!ObjectUtils.isEmpty(tokenResponse.getBody())){
Oauth2TokenResponse oauth2TokenResponse = gson.fromJson(tokenResponse.getBody(), Oauth2TokenResponse.class);
return oauth2TokenResponse;
}
return null;
}
请求参数:
{
"grant_type": [
"authorization_code"
],
"client_id": [
"zsw-cloud-dev"
],
"redirect_uri": [
"https://zsw-cloud.com/auth.html"
],
"code": [
"Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN-aSp50.ERmaVzBEgjbZ84FaGzQi4BMseKj8tz6kKVbBUh4PDiw"
]
}
响应值:
{
"access_token": "nRNAO1g8LNMI2i_FJXQDoLxvHL7aLz4sILhWoL_de4w.EcGjSbAlCmsBSPlE_KtNk99AcFVaE_eqye0Sh41dXSk",
"expires_in": 299,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDk2N2ZkYi0yYTg0LTRhNjAtODRlZi02M2Q3MGFmNTRmNzIifQ.eyJhdF9oYXNoIjoia05wVkVkWGhTa2U4MmdvSk81SUFqUSIsImF1ZCI6WyJ6c3ctY2xvdWQtZGV2Il0sImF1dGhfdGltZSI6MTY1ODYyOTIxOSwiZXhwIjoxNjU4NjI5NTI2LCJpYXQiOjE2NTg2MjkyMjYsImlzcyI6Imh0dHBzOi8venN3LWh5ZHJhLmNvbS8iLCJqdGkiOiI1ZmIwMWM4NC1mNTZmLTQ0MGUtYjFmZC1iOTBjODNhY2ZkODIiLCJyYXQiOjE2NTg2MjkyMTIsInNpZCI6IjU2OWY2ZDczLTgzZDEtNDBlOS04YmMxLWFlMjFhY2QzOGYyZCIsInN1YiI6InpzdyJ9.OaAlvwFY84BU2_fF8RxsXK_ueoURmvIMl_Xa7xZ566laeZdJ8GyONzrlGDSLwNNhdKV8Mcl3U8aNoGZDb5w3DRca9C0rqaedo-r4zMrsAZ-YNUAXvuv_Ga-n_MDPA2FxLF0vz1Til48jkbWhQ0QmJnT_m6DvUo4veVjtbU6Ggbz2-rYO7adW2rp1gf4I_AwwUOjfBtQmqZRPNvQIkX-Md-bQfhqnGikMEkeoZdYuZP3ags6H1cm3E8eMLyJk4kGXGkMosSKLE8LFh1HrXYQfCDwCVpL1dy_-b0ZKyj20RVVdusBzdb97MV4QFeKleuyGIRBXHI0etW9EELOVjPWcz59tuE29uToSopiEArFpeCotsh4nllFxqtvqRM4zh5ZMjf6MIHpm74IW8nVlXdCVjBjzZp3Lg3th7iWEDrZm_9tZ1o0SmYYwf9IbjjttrIaBbph-iTm5aijN6WHrKM0HNOcrERrcK4REcSFKueL46-yHRKmOhwXNROJHZQu3mTpZRO8BnR3eWBsRuFmVGLt8BKi8s_fAR7AI__WN1y8rek2_34LnAVrh8CJQnzBAIB-9y6AeGH8a9t_tqxkJWeLa8ohXVH8VTceKkCMNm_7x9vvhhqlb8lyVau9ktvkIgoalyGRmBf66FZQkxpDFht0XiC7ZGq9IusI-fDSIcGRuJa8",
"refresh_token": "emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M",
"scope": "openid offline",
"token_type": "bearer"
}
前端截图:
至此我们就获取到了访问令牌access_token,前端可以将令牌缓存在cookie或session中,相应的后台也会缓存,后面前端调用其他服务时携带令牌调用接口,后台校验根据token来判断是否放行。
光看接口大家可能不是特别明白,以下是具体的交互流程图:
以上就是获取token的整个流程。
当然token也有过期的时候,下面说一下刷新令牌,接口和上面获取token是同一个https://zsw-hydra.com:4444/oauth2/token,只是传参不同
请求参数:
{
"grant_type": [
"refresh_token"
],
"client_id": [
"zsw-cloud-dev"
],
"redirect_uri": [
"https://zsw-cloud.com/auth.html"
],
// refresh_token是获取token时的refresh_token,不是access_token
"refresh_token": [
"emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M"
]
}
响应值:
{
"access_token": "1ZhsazuFh-BLieBjJx6TsAGu2UxEvaG3obnbOTVdgNs.lF8mOodvdlNv9qWfBJHLEuONC3QdAub6WEoYBzMyEwo",
"expires_in": 299,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDk2N2ZkYi0yYTg0LTRhNjAtODRlZi02M2Q3MGFmNTRmNzIifQ.eyJhdF9oYXNoIjoia3VMQU1hTml1RGV2aDdpZEJvckVPQSIsImF1ZCI6WyJ6c3ctY2xvdWQtZGV2Il0sImF1dGhfdGltZSI6MTY1ODYzMDQ0NSwiZXhwIjoxNjU4NjMwOTY1LCJpYXQiOjE2NTg2MzA2NjUsImlzcyI6Imh0dHBzOi8venN3LWh5ZHJhLmNvbS8iLCJqdGkiOiI2ODVmMGNjNi04NmIwLTQwMzktOGVkMi0xMjM4ZGNmYTQ1ZGUiLCJyYXQiOjE2NTg2MzA0NDEsInNpZCI6Ijc4NzhjOWU2LTNlYzgtNDNmNC05OWNmLTBjNTZiYzM5NjFkOCIsInN1YiI6InpzdyJ9.oIo3uPJ_ZrKCUGymqSwHqAODvIziZvA3lVMAoONdDtF038zklOrSrDQlcsuUBILw_KJWJ4Ugu1HtcRroNDbX7FOo81r8IlEOKLJAgEG_ZL7TKbbNO6X7NYYyklL-85_OAzaerNhtbqtnwrJh0B9CjZIBQJDaeq4IYXE-otAdqtUcPq6ioG8tkaSyGfWHRtU_sdAKV03jXuJ6FlxihGNyt1Ei-BQdJrzr9Vt7lq4DKE1SF3OoAO9Yz7EIDlEnqL7vlbvvKXXIbH3QniqRqUszFCBZKkYdzCG12UDw3k0QYaE070YjeyKou6ZS4udOpXhoudKZYCHHxyx-Gyms_SkiBVGbqQHbYIflDest5mHgnTWD8ilnelBE7N0SznPw9OFWMzSC_DX0fv2N79xZA8r_hsB_Ul7nU5sNaH6QtZcmo0QMqUgPFAT7KQ9SFlyZ8dbFH4E6RyJzf31uDjHMc1ifhOQApprG6oY6ckgv1emkpapQB-lnVYTu-CDtB3MW8mQzrtL0SX9CPAsqYPeQf2050NFxWHl7jp-THoykTsbXRQZZHt0P95Y2mc-1fWwJ0haPcGqYsGsht1Gvm-dcITmtptWCmy0oEb0z_n9uYN_gLbxdxEXoB92FzEZufkd8xUTyBaLhYajlki0NFKJiBHFssIG13QD9ZRARKiqorKYZURU",
"refresh_token": "PL-InQes9HaSgVMH__c2ZqYfN9NTww2pbfooP3aqCp8.aS69mBivZgjdtK2vheZEjStIbnS7r3yo15C4gxSxzFc",
"scope": "openid offline",
"token_type": "bearer"
}
刷新token需要注意的是之前的refresh_token也会同时刷新
下面说一下另外几个跟token相关的接口:
删除token,可用于用户登出
接口路径:https://zsw-hydra.com:4444/oauth2/revoke
请求方式:POST
请求参数:
// client_id和client_secret 放在请求头中
{
"token": [
"1ZhsazuFh-BLieBjJx6TsAGu2UxEvaG3obnbOTVdgNs.lF8mOodvdlNv9qWfBJHLEuONC3QdAub6WEoYBzMyEwo"
]
}
代码实操:
public void revokeToken(String token) {
Gson gson = new Gson();
String client_id = "zsw-cloud-test";
String client_secret = "507e1d29-1460-4eab-b706-f3b411bc1717";
String hydraPublicRevokeTokenUrl = publicUrl + "/oauth2/revoke";
log.info("============POST================请求路径:{}", hydraPublicRevokeTokenUrl);
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.put("token",Lists.newArrayList(token));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBasicAuth(client_id, client_secret);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
log.info("============POST================/oauth2/revoke入参:{}", gson.toJson(map));
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, httpHeaders);
ResponseEntity<String> revokeTokenResponse = restTemplateIgnoreSSL.exchange(hydraPublicRevokeTokenUrl, HttpMethod.POST, httpEntity, String.class);
log.info("============POST================/oauth2/revoke出参:{}", revokeTokenResponse);
if (Objects.equals(revokeTokenResponse.getStatusCode(),HttpStatus.OK)){
log.info("================================删除成功!=====token:{}",token);
}
token自省,可用于获取用户标识或校验token是否存活
接口路径:https://zsw-hydra.com:4445/oauth2/introspect
请求方式:POST
请求参数:
{
"token": [
"1ZhsazuFh-BLieBjJx6TsAGu2UxEvaG3obnbOTVdgNs.lF8mOodvdlNv9qWfBJHLEuONC3QdAub6WEoYBzMyEwo"
]
}
响应值:
{
"active": true,
"scope": "openid offline",
"client_id": "zsw-cloud-dev",
"sub": "zsw",
"exp": 1658631946,
"iat": 1658631645,
"nbf": 1658631645,
"aud": [ ],
"iss": "https://zsw-hydra.com/",
"token_type": "Bearer",
"token_use": "access_token"
}
代码实操:
public OAuth2TokenIntrospection introspectOAuth2TokenCall(OAuth2TokenIntrospectionRequest tokenIntrospectionRequest) {
Gson gson = new Gson();
String hydraAdminIntrospectTokenUrl = adminUrl + "/oauth2/introspect";
log.info("============POST================请求路径:{}", hydraAdminIntrospectTokenUrl);
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.put("token",tokenIntrospectionRequest.getToken());
if (!ObjectUtils.isEmpty(tokenIntrospectionRequest.getScope())){
map.put("scope", Lists.newArrayList(tokenIntrospectionRequest.getScope()));
}
log.info("============POST================/oauth2/introspect入参:{}", gson.toJson(map));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, httpHeaders);
ResponseEntity<String> introspectTokenResponse = restTemplateIgnoreSSL.exchange(hydraAdminIntrospectTokenUrl, HttpMethod.POST, httpEntity, String.class);
log.info("============POST================/oauth2/introspect出参:{}", introspectTokenResponse);
if (!ObjectUtils.isEmpty(introspectTokenResponse.getBody())){
OAuth2TokenIntrospection tokenIntrospection = gson.fromJson(introspectTokenResponse.getBody(), OAuth2TokenIntrospection.class);
if (tokenIntrospection.getActive()){
String iat = LocalDateTime.ofEpochSecond(tokenIntrospection.getIat(),0, ZoneOffset.of("+8")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String exp = LocalDateTime.ofEpochSecond(tokenIntrospection.getExp(),0, ZoneOffset.of("+8")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info("=====================================token生成时间:{}",iat);
log.info("=====================================token过期时间:{}",exp);
log.info("=====================================用户标识:{}",tokenIntrospection.getSub());
}else {
log.info("=====================================token已失效:{}",tokenIntrospectionRequest.getToken());
}
return tokenIntrospection;
}
return null;
}
总结
以上就是博主总结的hydra获取授权token流程,由于网上关于hydra的资料非常少,中间碰到了很多大大小小的坑,在官网搜了半天才搜到。本文并没有讲述登录、续期、登出完整流程,只是说明一些API具体的交互过程与细节,实际如何搭配使用,还是要根据项目环境进行适配。大家如果在教程中遇到了那些问题也可以评论或者私信我,我看到后可以跟你一起去百度探索😁
说到最后推荐一款同样优秀的OAuth 2.0开源框架 Keycloak
相比于hydra网上资料要丰富很多,并且还带有可视化页面,感兴趣的同学可以自行了解一下。
本文参考:Ory Hydra 详解之入门