微服务Token方案之ORY Hydra授权中心_Java实现

前言

网上微服务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端点服务监听的端口号,默认是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分钟,正式环境不会设置这么短

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环境。

Client API官方文档地址

以下代码中出现的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基本功,下面主要介绍下授权流程。

四、认证授权流程及相关接口

先去官方取图:

 从上图可以看出,获取授权码一共需要两个流程:LoginConsent

下面我们就开始完整的演示一遍获取授权码的接口流程:

首先调用起点接口获取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重定向请求,调用成功后会携带login_challenge重定向到我们设置的URLS_LOGIN:

https://zsw-cloud.com/auth/login

代码截图:

这里我简单画了一个登录from,并且重定向到这个页面(主要是为了模拟登录场景)

登录页面如下

用户在前端输入用户密码后,点击登录会调用上面截图中的/userLogin接口,在用户登录接口中可以写一些账号密码、ip校验相关的校验,如果校验通过,下一步就是携带用户信息和login_challenge调用acceptLoginRequest接口:

请求路径: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请求,会携带consent_challenge重定向到我们设置的URLS_CONSENT:

https://zsw-cloud.com/auth/consent

代码截图:

 在/consent接口中我们需要携带和授权范围参数调用acceptConsentRequest接口完成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

该接口会携带code(授权码)重定向到我们的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时参数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 详解之入门

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值