基于private_key_jwt的客户端身份验证方法

news2024/11/16 4:21:49

参考文档 spring-authorization-server官网 【版本1.2.2】、 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants规范。
针对spring-authorization-server官网在Core Model / Components部分提到的RegisteredClient对象中涉及到clientAuthenticationMethods(客户端身份验证方法)属性支持的几种身份验证方法:【 client_secret_basic, client_secret_post, private_key_jwt, client_secret_jwt, and none (public clients)】。此次单独针对private_key_jwt这种方式来了解是如何达到客户端验证目的的。

介绍

通过官网以及参考其他网站,了解到基于private_key_jwt的身份验证方法的处理流程是客户端拥有自己的密钥对(公钥和私钥)。私钥由客户端自己保管,而公钥可以通过端点的方式向任何人公开。客户端使用算法(例如:RSA)通过私钥对客户端相关信息进行签名然后生成JWT(JSON Web Token),然后基于JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants 规范中提到Using JWTs for Client Authentication用法向授权服务器提交相关数据,例如:

POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=n0esc3NRze7LTCu7iYzS6a5acc3f0ogp4&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3A
client-assertion-type%3Ajwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.
eyJpc3Mi[...omitted for brevity...].
cC4hiUPo[...omitted for brevity...]

参数描述
grant_typeauthorization_code (固定值)
client_assertion_typeurn:ietf:params:oauth:client-assertion-type:jwt-bearer(固定值)
code授权码,需要访问授权服务器的授权码端点(/oauth2/authorize)来获取
client_assertion上面提到的通过私钥对客户端相关信息进行签名然后生成JWT

除了上面的数据外,在spring-authorization-server中还需要提供其他的数据:

参数描述
client_id客户端标识符
redirect_uri客户端信息中已注册重定向URI(对应RegisteredClient对象的redirectUris),例如接收授权码的重定向的URI

授权服务器在接收到上面的数据后主要会验证client_assertion(对应客户端生成的JWT),具体体现是请求经过授权服务器的过滤器链,被OAuth2ClientAuthenticationFilter拦截到,然后把请求的信息由JwtClientAssertionAuthenticationConverter转换为OAuth2ClientAuthenticationToken对象,然后交由JwtClientAssertionAuthenticationProvider来验证。验证方式是,授权服务器会根据client_id找到已在授权服务器注册的客户端信息,然后根据客户端注册时提供的jwkSetUrl【settings.client.jwk-set-url】拿到客户端暴露的公钥JWK(JSON Web Key)集端点地址,然后请求此地址拿到对应的公钥集。然后通过公钥对client_assertion进行验证签名,最后来达到客户端身份验证的目的。

项目搭建

为了验证上面的流程,我们需要创建两个SpringBoot项目,一个客户端,一个授权服务器。

客户端

创建SpringBoot项目,可参考我创建的项目client-endpoints。

然后我们需要为客户端生成密钥对,在cmd窗口执行如下命令:

keytool -genkeypair -keystore my.jks -storepass 123456 -alias my-key -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 365 -v

输入完命令后,窗口会让我们输入一些信息,例如:单位、组织等等,可随便填入一些信息,最后输入确认Y,会在对应的磁盘下生成一个my.jks文件,把文件赋值到项目的src/main/resources文件夹下。

我们需要加载自定义密钥对,如下:

public static KeyPair loadRsaKey() {
   KeyPair keyPair;
    try {
        ClassPathResource resource = new ClassPathResource("my.jks");
        KeyStore ks = KeyStore.getInstance("jks");
        ks.load(resource.getInputStream(), "123456".toCharArray());
        PrivateKey priKey = (PrivateKey) ks.getKey("my-key", "123456".toCharArray());
        PublicKey pubKey = ks.getCertificate("my-key").getPublicKey();
        keyPair = new KeyPair(pubKey, priKey);
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
    return keyPair;
}

创建两个端点,一个用来暴露公钥集,一个用来接收授权码,分别对应JwksController和Oauth2CodeController。

@RestController
public class JwksController {

    @GetMapping("client/jwks")
    public String jwkSet() {
        KeyPair keyPair = JwtUtil.loadRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return jwkSet.toString();
    }
}
@RestController
public class Oauth2CodeController {

    /**
     * 获取授权码
     *
     * @return 授权码
     */
    @GetMapping("client/oauth2/code")
    public String getCode(@RequestParam("code") String code) {
        return code;
    }
}

然后我们把客户端项目启动,访问地址http://127.0.0.1:8089/client/jwks(我的项目端口为8089)会返回公钥(JWK)集,如下:

{"keys":[{"kty":"RSA","e":"AQAB","kid":"12cc5247-86f6-4bdd-a7f3-1e77c8e62acb","n":"9HD79HaQ8DXOkNLZ7N-gcn8ruHuULDa6yUNYDCYFIwZXSdSYzRUMZGijkRUJXBHCRbDsa2GsleGLI4O7OWQCYmvNEWcrvy5zSDs-GJn5w7JjTqvEUrKVlFgDu8ASOF0B3YP0AYfzUjlZ7rgAVMKESZUxoAImRjj7mjj9TkTAGeBWRnlbbEYplSenlKbu3bLlVKb9UdUIH4IhCs6rTPkMf4UJLX4eWYJR6SPXFmLnKJF8kvGThTKtgU8R90O0jBrRoa8I_mXLaa1zV8tbMpYeOefmkX2RAaRU_yZRFoN2MUFmPE0BSQq0AJ08nxR8FHJSvw40-eVbhi47Ol10kxCbbw"}]}

授权服务器

创建SpringBoot项目,可参考我创建的项目protocol-endpoints-demo。当然你也可参考spring-authorization-server官网提供的Getting Started来搭建。唯一的区别是我们要自定义RegisteredClientRepository,如下:

@Bean
public RegisteredClientRepository registeredClientRepository() {

    RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("oidc-client")
            .clientSecret("{noop}secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            // 返回授权码的回调客户端地址
            .redirectUri("http://127.0.0.1:8089/client/oauth2/code")
            .scope("client.create")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true)
                    .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
                    // 配置公钥获取地址,需要获取公钥集来验证jwt(验签)
                    .jwkSetUrl("http://127.0.0.1:8089/client/jwks")
                    .build())
            .tokenSettings(TokenSettings.builder()
                    .accessTokenTimeToLive(Duration.ofHours(4l)).build())
            .build();

    return new InMemoryRegisteredClientRepository(oidcClient);
}

这里有几个属性我们需要注意下,我们需要设置对应private_key_jwt的clientAuthenticationMethod,redirectUri中指定接收授权码的重定向URI,还有jwkSetUrl来指定客户端暴露公钥集的端点。

启动项目,此项目我的端口为:6004。

生成JWK

此时我们需要根据授权服务器已注册的客户端信息生成JWK,如下:

public static JWTClaimsSet getJWTClaimsSet() {
     String clientId = "oidc-client";
     
     List<String> aud = new ArrayList<>();
     aud.add("http://127.0.0.1:6004");
     aud.add("http://127.0.0.1:6004/oauth2/token");
     aud.add("http://127.0.0.1:6004/oauth2/introspect");
     aud.add("http://127.0.0.1:6004/oauth2/revoke");

     // 前四个属性是必须的(iss、sub、aud、exp),参考JwtClientAssertionDecoderFactory#defaultJwtValidatorFactory
     JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
             // 发行者:固定clientId
             .issuer(clientId)
             // 主体:固定clientId
             .subject(clientId)
             // 授权服务器的相关地址
             .audience(aud)
             // 过期时间 24h
             .expirationTime(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
             // 访问时间
             .issueTime(new Date())
             // 范围
             .claim("scope", new String[]{"client.create"})
             .claim("jwk-set-url", "http://127.0.0.1:8089/client/jwks")
             .build();
     return claimsSet;
 }

这里需要注意下:issuer、subject、audience、expirationTime这四个属性是必须的,而且audience只能指定对应授权服务器对应的端点,如上所示,否则授权服务器验证不通过。可具体可参考JwtClientAssertionDecoderFactory#defaultJwtValidatorFactory来了解授权服务器是如何对上面四个属性进行规则校验的。

/**
 * 使用RSA算法加签生成JWT(JSON WEB TOKEN)
 */
public static String rsaSign(JWTClaimsSet claimsSet) throws JOSEException {
    KeyPair keyPair = loadRsaKey();
    RSASSASigner signer = new RSASSASigner(keyPair.getPrivate());

    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
    signedJWT.sign(signer);
    String token = signedJWT.serialize();
    return token;
}

通过上面的rsaSign方法返回的值就是客户端所需要的JWK,如下:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJvaWRjLWNsaWVudCIsImF1ZCI6WyJodHRwOi8vMTI3LjAuMC4xOjYwMDQiLCJodHRwOi8vMTI3LjAuMC4xOjYwMDQvb2F1dGgyL3Rva2VuIiwiaHR0cDovLzEyNy4wLjAuMTo2MDA0L29hdXRoMi9pbnRyb3NwZWN0IiwiaHR0cDovLzEyNy4wLjAuMTo2MDA0L29hdXRoMi9yZXZva2UiXSwiandrLXNldC11cmwiOiJodHRwOi8vMTI3LjAuMC4xOjgwODkvY2xpZW50L2p3a3MiLCJzY29wZSI6WyJjbGllbnQuY3JlYXRlIl0sImlzcyI6Im9pZGMtY2xpZW50IiwiZXhwIjoxNzEwNDc0MDYzLCJpYXQiOjE3MTAzODc2NjN9.Epx4rjjHfs-pwLWYfdukXAm_C-TQaCT9mBlMDN6RLuJJFDsBsluSXNda5-g8i01-rEhsKfvqf4y7aqgIl_YHRoRmYVgZDepvpsoqJ1AOgKgOZOQGNTpQGxV4eQZk-x3ZOGjhHqNdSp3cxjERE4aFcfp0SYYEen-_hEU6MN6AUJS1CauLPnJADTSlRer0A4qfeqMcAvEqF73AhUgcnHjVLqNjBdVhIkzc365dUXlVID51sZP4jfKSorz-LEr1Sv9iIw5ooKiSgRYCDP0-3e0hF97UOrUojO2FI_ObH4q2FpjaE5GjI3j6Gt-C6MyHoY9L0Rm-DAuYGzhG4jtaF9tP2A

流程演示

获取授权码

请求地址:

http://127.0.0.1:6004/oauth2/authorize?response_type=code&client_id=oidc-client&scope=client.create&redirect_uri=http%3A%2F%2F127.0.0.1%3A8089%2Fclient%2Foauth2%2Fcode

此时会跳转到登录端点,输入用户名:user,密码:password,进行登录。
在这里插入图片描述
在这里插入图片描述
授权码会追加在链接http://127.0.0.1:8089/client/oauth2/code后,code参数为授权码。
在这里插入图片描述

客户端身份验证

请求端点:http://127.0.0.1:6004/oauth2/token。我是通过Fetch API方式在浏览器控制台请求的:

fetch("http://127.0.0.1:6004/oauth2/token", {"headers": {"content-type": "application/x-www-form-urlencoded; charset=UTF-8",},"method": "POST","body":"grant_type=authorization_code&code=SXv7dM9svhSWyvKuIj2ED_TjwP3ZycOYVPS1eZrZ4tLnMvu9PSkxfcyZD2DAw6CaUX0tsqIrHCEZCCEYOW_UpOAJ73wzekFhIz2InZFIF1jox4SeBH10gUYXzGLne_QI&redirect_uri=http%3A%2F%2F127.0.0.1%3A8089%2Fclient%2Foauth2%2Fcode&client_id=oidc-client&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJvaWRjLWNsaWVudCIsImF1ZCI6WyJodHRwOi8vMTI3LjAuMC4xOjYwMDQiLCJodHRwOi8vMTI3LjAuMC4xOjYwMDQvb2F1dGgyL3Rva2VuIiwiaHR0cDovLzEyNy4wLjAuMTo2MDA0L29hdXRoMi9pbnRyb3NwZWN0IiwiaHR0cDovLzEyNy4wLjAuMTo2MDA0L29hdXRoMi9yZXZva2UiXSwiandrLXNldC11cmwiOiJodHRwOi8vMTI3LjAuMC4xOjgwODkvY2xpZW50L2p3a3MiLCJzY29wZSI6WyJjbGllbnQuY3JlYXRlIl0sImlzcyI6Im9pZGMtY2xpZW50IiwiZXhwIjoxNzEwNDc0MDYzLCJpYXQiOjE3MTAzODc2NjN9.Epx4rjjHfs-pwLWYfdukXAm_C-TQaCT9mBlMDN6RLuJJFDsBsluSXNda5-g8i01-rEhsKfvqf4y7aqgIl_YHRoRmYVgZDepvpsoqJ1AOgKgOZOQGNTpQGxV4eQZk-x3ZOGjhHqNdSp3cxjERE4aFcfp0SYYEen-_hEU6MN6AUJS1CauLPnJADTSlRer0A4qfeqMcAvEqF73AhUgcnHjVLqNjBdVhIkzc365dUXlVID51sZP4jfKSorz-LEr1Sv9iIw5ooKiSgRYCDP0-3e0hF97UOrUojO2FI_ObH4q2FpjaE5GjI3j6Gt-C6MyHoY9L0Rm-DAuYGzhG4jtaF9tP2A"}).then(res=>res.json()).then(json=>console.log(json));

在这里插入图片描述

JWT验证通过,然后在经过授权服务器一系列过滤器的处理最后返回访问token等相关数据,如上图标红所示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1515840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot+vue学生选课系统 java+ssm+idea+_mysql

系统包含三种角色&#xff1a;管理员、老师、学生&#xff0c;系统分为前台和后台两大模块&#xff0c;主要功能如下。 ide工具&#xff1a;IDEA 或者eclipse 编程语言: java 学生网上选课系统可以实现教室管理&#xff0c;老师管理&#xff0c;课程管理&#xff0c;教学计划管…

基于FPGA的OV7725摄像头的HDMI显示(含源码)

1、概述 本文FPGA通过SCCB接口初始化OV7725摄像头寄存器&#xff0c;然后采集OV7725的摄像头数据&#xff0c;使用DDR3对数据进行暂存&#xff0c;最后将数据输出到HDMI显示器上进行显示。 该工程对应系统框图如下所示&#xff0c;主要包含OV7725驱动及数据处理模块、DDR3读写控…

数据结构(二)——线性表(双链表)

2.3.3 双链表 单链表&#xff1a;单链表结点中只有一个指向其后继的指针&#xff0c;使得单链表只能从前往后依次遍历,无法逆向检索&#xff0c;有时候不太方便 双链表的定义&#xff1a;双链表结点中有两个指针prior和next&#xff0c;分别指向其直接前驱和直接后继 表头结点…

5分钟搞懂MySQL存储引擎

文章目录 什么是存储引擎&#x1f44b;&#xff1f;指定存储引擎✅查看mysql提供什么存储引擎查看mysql当前默认的存储引擎修改mysql默认的存储引擎设置表的存储引擎 常用存储引擎&#x1f9f0;InnoDBMyISAMMemoryInnoDB 和 MyISAM的区别 什么是存储引擎&#x1f44b;&#xff…

23万条数据集,可以用来区分钓鱼网站!

文章目录 一、何为钓鱼网站&#xff1f;二、数据集介绍引用数据集数据展示字段解释 三、数据分析数据读取使用ucimlrepo读取数据 四、下载地址 一、何为钓鱼网站&#xff1f; 在数字化时代&#xff0c;网络安全问题日益严重&#xff0c;其中钓鱼网站是一种常见的网络威胁。钓鱼…

基于Java (spring-boot)的进销存管理系统

一、项目介绍 首页&#xff0c;基础信息管理&#xff0c;备忘录&#xff0c;进销管理&#xff0c;仓库管理&#xff0c;系统管理 二、作品包含 三、项目技术 后端语言&#xff1a;Java 项目架构&#xff1a;B/S架构 数据库&#xff1a;MySQL 前端技术&#xff1a;Vue 后端技术&…

【C语言】比较两个字符串大小,strcmp函数

目录 一&#xff0c;strcmp函数 1&#xff0c;strcmp函数 2&#xff0c;函数头文件&#xff1a; 3&#xff0c;函数原型&#xff1a; 4&#xff0c;返回取值&#xff1a; 二&#xff0c;代码实现 三&#xff0c;小结 一&#xff0c;strcmp函数 1&#xff0c;strcmp函数 …

信道模拟器广泛应用于通信产业 我国企业竞争力不断提高

信道模拟器广泛应用于通信产业 我国企业竞争力不断提高 信道模拟器&#xff0c;模拟通信信道受环境因素影响产生各种特征的仪器&#xff0c;主要由接收电路、发射电路、模拟器、主控CPU等组成&#xff0c;可用于外场环境或者实验室环境中。 根据新思界产业研究中心发布的《202…

线性代数 --- 特征值与特征向量(下)

特征值与特征向量 Eigen Values & Eigen Vectors Part III:如何求解特征向量与特征值 The Key Equation 对于一般矩阵A&#xff0c;如何找到他的特征值与特征向量&#xff1f; Step I: Find λ first! 首先&#xff0c;我们有方程&#xff1a; 但这里有两个未知数&…

短视频解析接口分发系统

宝塔面板&#xff1a;Nginx系统 php7.2 Mysql 5.6-5.7 伪静态Thinkphp 上传文件直接访问域名安装即可 可以自备 听说后边要出saas去水印小程序 下载地址&#xff1a;https://pan.xunlei.com/s/VNskSEelfRVIzoSm5P5Rcw34A1?pwdqzhh# 接口演示&#xff1a; 前端演示…

前端入职配置新电脑!!!

前端岗位入职第一天到底应该做些什么呢&#xff1f;又该怎样高效的认识、融入团队&#xff1f;并快速进入工作状态呢&#xff1f;这篇文章就来分享一下&#xff0c;希望对即将走向或初入前端职场的你&#xff0c;能够有所帮助。内含大量链接&#xff0c;欢迎点赞收藏&#xff0…

GPU性能测试中的张量和矩阵运算

正文共&#xff1a;888 字 7 图&#xff0c;预估阅读时间&#xff1a;1 分钟 前面我们使用PyTorch将Tesla M4跑起来之后&#xff08;成了&#xff01;Tesla M4Windows 10AnacondaCUDA 11.8cuDNNPython 3.11&#xff09;&#xff0c;一直有个问题&#xff0c;那就是显存容量的问…

Springboot的配置文件及其优先级

配置文件 内置配置文件 配置文件的作用&#xff1a;修改SpringBoot自动配置的默认值&#xff1b;SpringBoot在底层都给我们自动配置好&#xff1b;SpringBoot使用一个全局的配置文件&#xff0c;配置文件名是固定的&#xff1a; application.propertiesapplication.yml 以上…

javaweb篇请求与相应的参数问题

目录 前言 简单传参设置 get请求无法识别 post请求 简单传参问题无法识别的解决问题 注意事项 改法 实体参数 代码展示 今日分享 前言 友友们&#xff0c;大家好&#xff0c;今天来开荒了,今天介绍的是在进行数据请求以及相应的时候&#xff0c;我们不仅仅只是进入一…

Java SE 抽象类与接口(二):接口(下)

2.5 实现多个接口 在Java语言中&#xff0c;类和类之间是单继承关系&#xff0c;一个类只可以有一个父类&#xff0c;即Java中不支持多继承关系&#xff0c;但是一个类可以实现多个接口&#xff0c;下面通过Animal类来具体说明 class Animal {protected String name;public A…

phpcms上传漏洞

原始漏洞 漏洞原理&#xff1a;我们上传一个zip的压缩包&#xff0c;它会解压然后删除其中不是.jpg .gig .png的文件 function check_dir($dir)&#xff1a;这是一个PHP函数的定义&#xff0c;它接受一个参数 $dir&#xff0c;代表要检查的目录路径。 $handle opendir($dir)…

MySQL学习Day32——数据库备份与恢复

在任何数据库环境中&#xff0c;总会有不确定的意外情况发生&#xff0c;比如例外的停电、计算机系统中的各种软硬件故障、人为破坏、管理员误操作等是不可避免的&#xff0c;这些情况可能会导致数据的丢失、 服务器瘫痪等严重的后果。存在多个服务器时&#xff0c;会出现主从服…

C语言程序环境和预处理Pt.1 - 预处理指令|预处理操作符

电脑所能识别的语言为二进制指令&#xff0c;而我们所写的C语言代码是文本信息。为了能使计算机识别并执行C语言代码&#xff0c;就需要翻译环境&#xff0c;使C语言代码翻译为二进制的指令。 1.按下编译按钮的幕后 - 程序的翻译环境 从C语言源代码到计算机可识别的二进制文件…

【前端】 响应式布局

目录 1.媒体查询 2.BootStrap 2.1BootStrap引入 2.2BootStrap栅格系统 2.3BootStrap手册查询 1.媒体查询 响应式布局&#xff1a;显示区域改变&#xff0c;布局随之改变&#xff0c;即同一套代码适配不同大小的显示器 媒体查询&#xff1a;检测视口宽度&#xff0c;设置差…

案例分析篇12:可靠性设计考点(2024年软考高级系统架构设计师冲刺知识点总结系列文章)

专栏系列文章推荐: 2024高级系统架构设计师备考资料(高频考点&真题&经验)https://blog.csdn.net/seeker1994/category_12593400.html 【历年案例分析真题考点汇总】与【专栏文章案例分析高频考点目录】(2024年软考高级系统架构设计师冲刺知识点总结-案例分析篇-…