前言
网上微服务token流程案例很多,但是关于流程的案例很多。hydra但是资料很少,本文主要讲解使用获取令牌token如果你不知道流程Oauth建议百度先了解一下。
这里简单说一下
OAuth2.0
授权代码是四种授权中最复杂、最完整、最严格的授权模式。它的特点是通过客户端的后台服务器和"授权服务商"交互认证服务器。
如上图所示,在授权码模式下,第三方软件可以获得资源所有者(用户)的授权码和注册时的授权码 client_id 和 client_secret 换回访问令牌 token 的值,这个token事实上,就像我们的核酸码一样,当核酸码过期时,需要刷新核酸码,重新获取核酸,否则门卫是不会让你进入公司大楼的。
一、hydra简单介绍
ORY Hydra经过强化,经过OpenID认证的OAuth 2.0服务器和OpenID Connect供应商优化了低延迟、高吞吐量和低资源消耗。ORY Hydra 不是身份提供商(用户注册、用户登录、密码重置过程),而是通过登录和同意应用程序连接到您现有的身份提供商。很容易用不同的语言登录和同意应用程序,并提供示例同意应用程序(Go,Node)和 SDK。
以上是官方原话:
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端点服务监控端口号默认为4445
URLS_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分钟,正式环境不会设置这么短
hydra安装授权服务器地址hydra的机器ip映射路径
前端项目域名微服务
:微服务后台auth认证中心服务请求地址
hydra默认使用,由于https会有证书问题,所以我在这里用。 将https转换成http请求,当然,如果你有域名,你可以去免费申请ssl证书不需要像被迫的博主那样使用openssl本地申请证书。
当然,如果想直接使用http也是可以的,只需要在命令最后加上 相应的url也要改成http。
# 9.最后查看日志是否正常启动 docker logs ory-hydra-zsw-dev
这里推荐使用docker可视化工具查看,比命令行要方便很多,安装教程网上有很多,这个请自行百度:
启动成功后hydra服务端就搭建成功了,为了我们后面的认证流程能够畅通无阻,这里建议
暂时关闭防火墙命令:systemctl stop firewalld
三、客户端相关接口
Hydra服务端搞定后,我们就需要操作客户端了,官方提供了对应的restful API文档
客户端可以这么理解:每个客户端都对应我们正常开发的dev、test、uat、prod环境。
:https://zsw-hydra.com:4445/clients
:POST
变量值:
这里需要注意的https请求默认是开启ssl验证的,所以我在ResTemplate配置中设置了绕过ssl验证,不然请求会报错,具体配置参考: RestTemplate绕过SSL证书校验
细心的同学可能会注意到swagger的参数是驼峰式,而我的代码日志中是下划线分割的,这里我用Gson反序列化了一下,因为
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(下面授权流程也会介绍)
创建完成后就可以看到数据库中表里已经存在我们新建的client了
: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();
}
: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;
}
: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;
}
: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_challenge:
https://zsw-hydra.com:4444/oauth2/auth?&client_id=zsw-cloud-dev&response_type=code&scope=openid&state=zsw123456789&redirect_uri=https://zsw-cloud.com/auth.html
参数说明:
client_id:就是我们上面新建的客户端id
response_type:响应值类型
scope:作用范围
state:这个可以随便写
redirect_uri:授权码重定向url
通过官网api介绍可以看出这是一个302重定向请求,调用成功后会携带重定向到我们设置的URLS_LOGIN:
https://zsw-cloud.com/auth/login
代码截图:
这里我简单画了一个登录from,并且重定向到这个页面(主要是为了模拟登录场景)
登录页面如下
用户在前端输入用户密码后,点击登录会调用上面截图中的/userLogin接口,在用户登录接口中可以写一些账号密码、ip校验相关的校验,如果校验通过,下一步就是携带用户信息和login_challenge调用接口:
https://zsw-hydra.com:4445/oauth2/auth/requests/login/accept?login_challenge=cdf848bb89da4d56802dd1bb52c322c7
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流程:
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
该接口也是302请求,会携带重定向到我们设置的URLS_CONSENT:
https://zsw-cloud.com/auth/consent
代码截图:
在/consent接口中我们需要携带和授权范围参数调用接口完成Consent流程:
https://zsw-hydra.com:4445/oauth2/auth/requests/consent/accept?consent_challenge=ae12fb06fecc46c885983f9451490140
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认证流程也结束了,下一步我们继续重定向到响应值接口:
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
该接口会携带重定向到我们的redirect_uri地址,然后我们就可以通过授权码获取认证token了
重定向路径如下:
https://zsw-cloud.com/auth.html?code=Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN-aSp50.ERmaVzBEgjbZ84FaGzQi4BMseKj8tz6kKVbBUh4PDiw&scope=openid+offline&state=zsw123456789
auth.html是我们的授权认证页面,这里我简单的画了一个
具体逻辑代码如下图:
页面效果如下图:
得到授权码code后,下面进入token流程。
五、token相关接口
我们在授权流程结束后拿到code授权码,下一步就是调用后端接口获取token并返回给前端https://zsw-cloud.com/auth/login/callback
接口代码截图:
上面我在新建client时参数的区别就在于一种需要将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需要注意的是之前的
下面说一下另外几个跟token相关的接口:
,可用于用户登出
https://zsw-hydra.com:4444/oauth2/revoke
// 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是否存活
https://zsw-hydra.com:4445/oauth2/introspect
{
"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网上资料要丰富很多,并且还带有可视化页面,感兴趣的同学可以自行了解一下。