PAC4J 新手入门,之三(自定义 OAuth2.0 客户端及 PAC4J 更多概念)

认证协议概述

我们常见的认证协议,包括 OAuthSAMLCASOpenID Connect 等。在这些协议中,OAuth 协议只定义了基本的协议架构,但没有明确要求 API 如何指定,因而针对不同的协议提供厂商,需要开发不同的客户端进行对接。这些客户端最大的差异在于用户 API 响应的数据定义千差万别,所以自定义客户端最主要的实现是对用户数据的解析。

本篇我们以对接 Gitee 的 OAuth2.0 为例,实现自定义客户端。

Gitee OAuth2.0 API

首先,我们先到 Gitee 官网,查看 OAuth2.0 相关 API 以及交互流程。Gitee 文档地址:Gitee OAuth

交互流程见下图:

API 说明见下图:

然后我们到 API 文档中查找用户 API,如下图:

然后我们将 API 列在下表:

用途 Http Method API
获取 code(Authorization) GET https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
获取 token(AccessToken) POST https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
用户数据 GET https://gitee.com/api/v5/user?access_token={access_token}

这里要特别注意一点,我们发现接口 https://gitee.com/oauth/token 的 Http Method 为 POST,但数据是以 URL 参数的形式传递的,这不符合常规设计,后续客户端代码开发时,不能依赖 pac4j 的基类实现,需要按框架其他方式实现这种 API 调用。

#创建 OAuth2.0 应用

我们参照 GitHub 的方式,在 Gitee 官网上同样创建一个 OAuth2.0 应用。在登录官网后,点击右上角用户下拉,点击"设置",然后找到"第三方应用",最终的入口地址为 第三方应用

然后我们创建一个新应用,如下图:

类似于 GitHub,"应用回调地址"需要填写项目的回调地址,我们当前项目在本地运行,所以回调地址填写为 http://localhost:8888/callback?client_name=GiteeClientGiteeClient 为后续我们定义的 pac4j 客户端名称。

应用创建后如下图:

创建客户端代码

因为 Gitee 与 GitHub 类似,所以我们参照 pac4j 中预置的 GitHubClient 进行开发。

GiteeAPI

首先,我们定义 API 类 GiteeApi,继承基类 DefaultApi20,代码如下:

java 复制代码
package tech.bookhub.client.gitee;

import com.github.scribejava.apis.GitHubApi;
import com.github.scribejava.core.builder.api.DefaultApi20;
import com.github.scribejava.core.httpclient.HttpClient;
import com.github.scribejava.core.httpclient.HttpClientConfig;
import com.github.scribejava.core.oauth.OAuth20Service;

import java.io.OutputStream;

public class GiteeApi extends DefaultApi20 {
    protected GiteeApi() {
    }

    private static class InstanceHolder {
        private static final GiteeApi INSTANCE = new GiteeApi();
    }

    public static GiteeApi instance() {
        return GiteeApi.InstanceHolder.INSTANCE;
    }


    @Override
    public String getAccessTokenEndpoint() {
        return "https://gitee.com/oauth/token";
    }

    @Override
    public String getAuthorizationBaseUrl() {
        return "https://gitee.com/oauth/authorize";
    }

    @Override
    public OAuth20Service createService(String apiKey, String apiSecret, String callback, String defaultScope,
                                        String responseType, OutputStream debugStream, String userAgent, HttpClientConfig httpClientConfig,
                                        HttpClient httpClient) {
        return new GiteeService(this, apiKey, apiSecret, callback, defaultScope, responseType, userAgent, httpClientConfig, httpClient);
    }
}

在代码中 getAccessTokenEndpointgetAuthorizationBaseUrl 为基类的抽象方法,用途为返回对应的 API。我们将 Gitee OAuth2.0 对应的 API 填写在此。

同时,我们参考 GitHubClient,将此类实现为单例。

另外,我们重载了方法 createService,并生成 GiteeService 类的对象,GiteeService 类在下面实现。

GiteeService

接着,我们实现 GiteeService 类,继承自基类 OAuth20Service。我们主要重载 createAccessTokenRequest 方法,此方法将用于解决 Gitee 的 API https://gitee.com/oauth/token 使用 Http Method 方法为 POST,但参数却在 URL 参数中传递,这种非常规的实现。

如果按照常规实现,POST 方法中,数据以 Request Body 的形式传递,GET 方法中,数据以 URL 参数传递,那么我们不用实现此类,也不用在 GiteeApi 中重载 createService 方法,pac4j 框架的基类已经实现了常规实现的参数拼接。

代码如下:

java 复制代码
package tech.bookhub.client.gitee;

import com.github.scribejava.core.builder.api.DefaultApi20;
import com.github.scribejava.core.httpclient.HttpClient;
import com.github.scribejava.core.httpclient.HttpClientConfig;
import com.github.scribejava.core.model.OAuthConstants;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.oauth.AccessTokenRequestParams;
import com.github.scribejava.core.oauth.OAuth20Service;

import java.util.Map;

public class GiteeService extends OAuth20Service {
    public GiteeService(DefaultApi20 api, String apiKey, String apiSecret, String callback, String defaultScope,
                        String responseType, String userAgent, HttpClientConfig httpClientConfig, HttpClient httpClient) {
        super(api, apiKey, apiSecret, callback, defaultScope, responseType, null, userAgent, httpClientConfig, httpClient);
    }

    @Override
    protected OAuthRequest createAccessTokenRequest(AccessTokenRequestParams params) {
        final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(), getApi().getAccessTokenEndpoint());

        getApi().getClientAuthentication().addClientAuthentication(request, getApiKey(), getApiSecret());

        request.addQuerystringParameter(OAuthConstants.CODE, params.getCode());
        final String callback = getCallback();
        if (callback != null) {
            request.addQuerystringParameter(OAuthConstants.REDIRECT_URI, callback);
        }
        final String scope = params.getScope();
        if (scope != null) {
            request.addQuerystringParameter(OAuthConstants.SCOPE, scope);
        } else if (getDefaultScope() != null) {
            request.addQuerystringParameter(OAuthConstants.SCOPE, getDefaultScope());
        }
        request.addQuerystringParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE);


        final Map<String, String> extraParameters = params.getExtraParameters();
        if (extraParameters != null && !extraParameters.isEmpty()) {
            for (Map.Entry<String, String> extraParameter : extraParameters.entrySet()) {
                request.addQuerystringParameter(extraParameter.getKey(), extraParameter.getValue());
            }
        }

        logRequestWithParams("access token", request);
        return request;
    }
}

在方法 createAccessTokenRequest 中,我们将基类中使用的 request.addParameter 修改为 request.addQuerystringParameter,以达到实现目的。

GiteeProfileDefinition

在 pac4j 框架中,用户数据最终会以 Profile 类进行展示,所以我们需要定义客户端如何获取用户数据,并添加到相应 Profile 对象中。我们先实现定义,再实现客户端对应的 Profile 类本身。

代码如下:

java 复制代码
package tech.bookhub.client.gitee;

import com.github.scribejava.core.model.Token;
import org.pac4j.core.profile.ProfileHelper;
import org.pac4j.core.profile.converter.Converters;
import org.pac4j.oauth.config.OAuthConfiguration;
import org.pac4j.oauth.profile.JsonHelper;
import org.pac4j.oauth.profile.definition.OAuthProfileDefinition;

import java.util.Arrays;

import static org.pac4j.core.profile.AttributeLocation.PROFILE_ATTRIBUTE;

public class GiteeProfileDefinition extends OAuthProfileDefinition {
    public static final String URL = "url";
    public static final String FOLLOWING = "following";
    public static final String PUBLIC_REPOS = "public_repos";
    public static final String AVATAR_URL = "avatar_url";
    public static final String LOGIN = "login";
    public static final String NAME = "name";

    public GiteeProfileDefinition() {
        super(x -> new GiteeProfile());

        Arrays.asList(new String[]{
                NAME, LOGIN, URL, AVATAR_URL
        }).forEach(a -> primary(a, Converters.STRING));

        Arrays.asList(new String[]{
                FOLLOWING, PUBLIC_REPOS
        }).forEach(a -> primary(a, Converters.INTEGER));
    }

    @Override
    public String getProfileUrl(Token accessToken, OAuthConfiguration configuration) {
        return "https://gitee.com/api/v5/user";
    }

    @Override
    public GiteeProfile extractUserProfile(String body) {
        final var profile = (GiteeProfile) newProfile();
        final var json = JsonHelper.getFirstNode(body);
        if (json != null) {
            profile.setId(ProfileHelper.sanitizeIdentifier(JsonHelper.getElement(json, "id")));
            for (final var attribute : getPrimaryAttributes()) {
                convertAndAdd(profile, PROFILE_ATTRIBUTE, attribute, JsonHelper.getElement(json, attribute));
            }
        } else {
            raiseProfileExtractionJsonError(body);
        }
        return profile;
    }
}

重载方法 getProfileUrl 中,我们填写 Gitee 用户 API,用于获取用户数据。然后定义用户数据相关字段,并在构造函数中配置相应字段的转换方法。代码中作为示例,没有配置 Gitee 用户 API 响应的所有字段,仅配置了部分字段。

最后重载的方法 extractUserProfile 则为解析用户 API 数据后,如何转换为对应的 Profile 对象,下面将实现 GiteeProfile

GiteeProfile

GiteeProfile 类为用户数据获取的定义类,它继承基类 OAuth20Profile

代码如下:

java 复制代码
package tech.bookhub.client.gitee;

import org.pac4j.oauth.profile.OAuth20Profile;

public class GiteeProfile extends OAuth20Profile {
    @Override
    public String getDisplayName() {
        return (String) getAttribute(GiteeProfileDefinition.NAME);
    }

    @Override
    public String getUsername() {
        return (String) getAttribute(GiteeProfileDefinition.LOGIN);
    }

    public Integer getFollowing() {
        return (Integer) getAttribute(GiteeProfileDefinition.FOLLOWING);
    }

    public Integer getPublicRepos() {
        return (Integer) getAttribute(GiteeProfileDefinition.PUBLIC_REPOS);
    }
}

GiteeClient

最后,我们定义客户端类 GiteeClient,此类用于配置客户端要使用的 API 定义以及 Profile 数据解析。

代码如下:

java 复制代码
package tech.bookhub.client.gitee;

import org.pac4j.core.util.HttpActionHelper;
import org.pac4j.oauth.client.OAuth20Client;
import org.springframework.util.StringUtils;

import java.util.Optional;

public class GiteeClient extends OAuth20Client {
    public static final String DEFAULT_SCOPE = "user_info";

    public GiteeClient() {
        setScope(DEFAULT_SCOPE);
    }

    public GiteeClient(String key, String secret) {
        setScope(DEFAULT_SCOPE);
        setKey(key);
        setSecret(secret);
    }

    public GiteeClient(String key, String secret, String scope) {
        setScope(DEFAULT_SCOPE);
        setKey(key);
        setSecret(secret);
        setScope(scope);
    }

    @Override
    protected void internalInit(boolean forceReinit) {
        configuration.setApi(GiteeApi.instance());
        configuration.setProfileDefinition(new GiteeProfileDefinition());
        configuration.setTokenAsHeader(true);
        defaultLogoutActionBuilder((ctx, session, profile, targetUrl) ->
                Optional.of(HttpActionHelper.buildRedirectUrlAction(ctx, "https://gitee.com/logout")));

        super.internalInit(forceReinit);
    }

    public String getScope() {
        return getConfiguration().getScope();
    }

    public void setScope(String scope) {
        getConfiguration().setScope(StringUtils.hasText(scope) ? scope : DEFAULT_SCOPE);
    }
}

验证客户端

我们在之前的 Pac4jConfig 类中添加代码使用 GiteeClient

代码如下:

java 复制代码
package tech.bookhub.config;

import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.oauth.client.GitHubClient;
import org.pac4j.springframework.security.web.Pac4jEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.bookhub.client.gitee.GiteeClient;

@Configuration
public class Pac4jConfig {

    @Bean
    public Config config() {
        GitHubClient gitHubClient = new GitHubClient("a85f19ea0f51face127a", "84bf0695ea2a62674b8d5961a02a4c793bf23e2a");

        GiteeClient giteeClient = new GiteeClient("da28980047eb2c732b8bcee4be567c6a4f38c6459587063f2607084c9c33b957",
                "4cd81eac1dae28b698044ed5b55e2580da94aca7d872e11e5b47d6c8a3b0a26d");

        Clients clients = new Clients("http://localhost:8888/callback", gitHubClient, giteeClient);
        Config config = new Config(clients);
        return config;
    }

    @Bean
    public Pac4jEntryPoint pac4jEntryPoint() {
        return new Pac4jEntryPoint(config(), "GitHubClient");
    }
}

然后在 SecurityConfig 类中配置相应的 url 匹配。

代码如下:

java 复制代码
    @Configuration
    @Order(4)
    public static class GiteeAuthAdapter extends WebSecurityConfigurerAdapter {
        @Autowired
        private Config config;

        protected void configure(HttpSecurity http) throws Exception {
            final SecurityFilter filter = new SecurityFilter(config,"GiteeClient");

            http.antMatcher("/login/gitee")
                    .addFilterBefore(filter, BasicAuthenticationFilter.class)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
        }
    }

最后,在 LoginController 类中增加相应的 API。

代码如下:

java 复制代码
    @GetMapping("/login/gitee")
    public List<UserProfile> loginGitee(final HttpServletRequest request, final HttpServletResponse response) {
        final JEEContext jeeContext = new JEEContext(request, response);
        final SpringSecurityProfileManager profileManager = new SpringSecurityProfileManager(jeeContext, JEESessionStore.INSTANCE);
        return profileManager.getProfiles();
    }

我们运行项目,在浏览器中输入 http://localhost:8888/login/gitee,将会跳转到 Gitee 认证界面,登录成功后,将会展示获取到的用户数据。如下图。

以上自定义客户端完成。

我们按照以上实现,将 pac4j 的 Gitee 客户端的开源组件上传到了 Maven 仓库,可以直接引用:

xml 复制代码
<dependency>
    <groupId>fun.mortnon.pac4j</groupId>
    <artifactId>oauth-gitee</artifactId>
    <version>1.0.0</version>
</dependency>

1.0.0 版本,编译 JDK 为 11,依赖的 pac4j 版本为 5.7.0;0.0.8 版本,编译 JDK 为 8,依赖的 pac4j 版本为 4.5.7

本文 Demo 内容见 GitHub,Demo 2

概念:客户端、授权器与安全配置

在之前的示例中,我们直接对接 pac4j,并让项目运行起来了。在 Pac4jConfig 中,我们最先的配置的即是客户端 Client。

java 复制代码
        GitHubClient gitHubClient = new GitHubClient("a85f19ea0f51face127a", "84bf0695ea2a62674b8d5961a02a4c793bf23e2a");

        GiteeClient giteeClient = new GiteeClient("da28980047eb2c732b8bcee4be567c6a4f38c6459587063f2607084c9c33b957",
                "4cd81eac1dae28b698044ed5b55e2580da94aca7d872e11e5b47d6c8a3b0a26d");

代码中我们配置了两个客户端 GitHubClientGiteeClient。我们为客户端配置了其对应的协议所需要参数,如代码中所使用的 OAuth2.0 协议的 keysecret。而对应的服务端的协议相关 API 则封装在客户端的相关实现类中。

然后我们需要将客户端添加到安全配置 Config 中,如下:

java 复制代码
        Clients clients = new Clients("http://localhost:8888/callback", gitHubClient, giteeClient);
        Config config = new Config(clients);

我们还可以为安全配置添加认证器 Authorizer,用于进一步细化认证配置,我们没有添加,使用默认的 csrfCheck

最后再依据不同的认证框架,将 Config 配置使用,如下:

java 复制代码
        protected void configure(HttpSecurity http) throws Exception {
            final SecurityFilter filter = new SecurityFilter(config,"GiteeClient");

            http.antMatcher("/login/gitee")
                    .addFilterBefore(filter, BasicAuthenticationFilter.class)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
        }

通过这几个配置就可以将整个认证对接起来。

概念:匹配器

匹配器 Matcher 可以用来做细化扩展,比如我们使用的 SecurityFilter,如下:

java 复制代码
final SecurityFilter filter = new SecurityFilter(config,"GiteeClient");

这个过滤器默认不会对请求的 URL 进行过滤,如果想细化控制,比如只针对某些 URL 生效,可以通过向 SecurityFilter 添加自定义 Matcher 来实现。当然,在特定的框架中,可以使用框架自己的特性来实现匹配,比如我们使用的 Spring Security 中,可以直接在 HttpSecurity 声明中配置。

概念:Profile

当 pac4j 成功认证用户时,将从认证提供者检索其数据,并构建用户配置文件,即 profile。用户配置文件包括:

  • 一个标识符 (getId())
  • 属性(getAttributes()、 getAttribute(name))
  • 认证相关属性 (getAuthenticationAttributes()、getAuthenticationAttribute(name))
  • 角色 (getRoles())
  • 权限 (getPermissions())
  • 客户端名称(getClientName())
  • 记住我 (isRemembered())
  • 一个关联标识符(getLinkedId())

每个客户端会自己定义一个对应的配置文件,这样当客户端获取到服务端响应的用户数据后,可以保存下来供项目代码使用。

概念:Web Context、Session Store

Web 上下文、会话存储,是比较全局的配置, 一般不需要在配置时指定。

结语

到此,就是 pac4j 的新手入门所有文章,通过这些简单的介绍,了解了 pac4j 的一些基础概念,并能基于 Spring Security 框架对接上 pac4j,还能照着现有的客户端实现,自定义一个客户端的实现。

而实现一个自定义客户端,会是实际工作中对接一个厂商 OAuth2.0 协议时最常做的事情。

另,我们针对 Java 云原生框架: Micronaut 框架 ,还提供了一个自定义的 Pac4j 适配,以支持 Micronaut Security 框架。大家也可以看一下我们针对一个认证框架,如何适配上 pac4j 的思路,项目参考:Micronaut-pac4j ,使用这个适配组件的 Demo 参考 Demo

相关推荐
java小吕布31 分钟前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy1 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄1 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫2 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
徐小夕2 小时前
Flowmix/Docx 多模态文档编辑器:V1.3.5版本,全面升级
前端·javascript·架构
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng2 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss2 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
Smile丶凉轩2 小时前
微服务即时通讯系统的实现(客户端)----(1)
微服务·云原生·架构
大鲤余3 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust