[{"data":1,"prerenderedAt":3332},["ShallowReactive",2],{"content:\u002F2026\u002F08\u002Fgo-jws":3,"surround:\u002F2026\u002F08\u002Fgo-jws":3326},{"id":4,"title":5,"body":6,"categories":3303,"date":3305,"description":3306,"draft":3307,"extension":3308,"image":3309,"meta":3310,"navigation":1589,"path":3312,"permalink":3309,"published":3309,"readingTime":3313,"recommend":3309,"references":3309,"seo":3318,"sitemap":3319,"stem":3320,"tags":3321,"type":3324,"updated":3305,"__hash__":3325},"content\u002Fposts\u002F2026\u002F08\u002Fgo-jws.md","Go 使用 JWS 与 RS256 实现私钥签名、公钥验签",{"type":7,"value":8,"toc":3276},"minimark",[9,13,21,30,36,41,44,54,61,66,69,110,113,119,129,143,147,154,240,243,247,250,256,263,269,272,278,281,287,290,293,320,324,327,358,455,458,462,465,546,597,600,607,613,624,628,631,639,642,647,650,654,657,663,666,705,714,753,765,768,774,777,781,784,880,883,935,947,950,1105,1114,1120,1124,1127,1174,1181,1184,1188,1194,1205,1208,1215,1218,1225,1253,1256,1262,1266,1269,1285,1288,1310,1313,1338,1341,1370,1373,1379,1390,1407,1410,1425,1428,1432,1439,1463,1821,1833,1837,1847,2152,2155,2159,2162,2248,2251,2403,2409,2413,2418,2603,2613,2619,2622,2644,2647,2651,2654,2807,2813,2816,2919,2922,2962,2965,2979,3002,3005,3011,3025,3031,3038,3041,3048,3125,3128,3131,3134,3195,3206,3209,3218,3221,3244,3247,3250,3272],[10,11,12],"p",{},"在接口鉴权、服务间通信和第三方回调等场景中，我们不仅需要传递数据，还需要确认数据确实来自可信的发送方，并且在传输过程中没有被修改。JWS（JSON Web Signature）正是用来解决这类问题的标准。",[10,14,15,16,20],{},"本文使用 Go 和 ",[17,18,19],"code",{"code":19},"github.com\u002Flestrrat-go\u002Fjwx\u002Fv4"," 完成一个最小示例：先用 OpenSSL 生成 RSA 密钥对，再使用 RS256 和私钥对 JSON 数据签名，最后使用公钥验证签名并还原原始数据。",[22,23,27],"alert",{"title":24,"type":25,":card":26},"阅读目标","info","true",[10,28,29],{},"读完本文，你将理解 JWS 的三段结构和 RS256 的签名流程，能够区分 JWT、JWS、JWE、JWK，并能在 Go 中完成 RSA 密钥解析、私钥签名与公钥验签。",[31,32],"link-card",{"description":33,"link":34,"title":35},"本文使用的 main.go、RSA 公私钥及运行说明","https:\u002F\u002Fcnb.cool\u002Fcodepzj\u002Fgoplayground\u002F-\u002Ftree\u002Fmain\u002Fjws","Go JWS 完整示例",[37,38,40],"h2",{"id":39},"jws-的三部分结构","JWS 的三部分结构",[10,42,43],{},"JWS 是一种为数据添加数字签名的标准。它最常见的紧凑序列化格式由三部分组成：",[45,46,52],"pre",{"className":47,"code":49,"language":50,"meta":51},[48],"language-text","BASE64URL(Protected Header).BASE64URL(Payload).BASE64URL(Signature)\n","text","",[17,53,49],{"__ignoreMap":51},[10,55,56,57,60],{},"这里的点号 ",[17,58,59],{"code":59},"."," 是三部分之间的分隔符。以前文的业务数据为例，这三部分的含义如下。",[62,63,65],"h3",{"id":64},"protected-header告诉接收方如何验签","Protected Header：告诉接收方如何验签",[10,67,68],{},"Protected Header 是一段 JSON 元数据。本例使用 RS256，因此签名前的内容类似：",[45,70,74],{"className":71,"code":72,"language":73,"meta":51,"style":51},"language-json shiki shiki-themes github-light github-dark","{\n  \"alg\": \"RS256\"\n}\n","json",[17,75,76,85,104],{"__ignoreMap":51},[77,78,81],"span",{"class":79,"data-line":80,"line":80},"line",1,[77,82,84],{"class":83},"sSDpw","{\n",[77,86,88,92,96,100],{"class":79,"data-line":87,"line":87},2,[77,89,91],{"class":90},"indent","  ",[77,93,95],{"class":94},"sj4cs","\"alg\"",[77,97,99],{"class":98},"sVt8B",": ",[77,101,103],{"class":102},"sZZnC","\"RS256\"\n",[77,105,107],{"class":79,"data-line":106,"line":106},3,[77,108,109],{"class":83},"}\n",[10,111,112],{},"它经过 Base64URL 编码后成为 JWS 的第一部分：",[45,114,117],{"className":115,"code":116,"language":50,"meta":51},[48],"eyJhbGciOiJSUzI1NiJ9\n",[17,118,116],{"__ignoreMap":51},[10,120,121,124,125,128],{},[17,122,123],{"code":123},"alg"," 表示签名算法。实际系统中还常用 ",[17,126,127],{"code":127},"kid"," 表示签名所使用的密钥编号，方便接收方在密钥轮换时找到对应公钥。",[10,130,131,132,136,137,139,140,142],{},"之所以称为 ",[133,134,135],"strong",{},"Protected"," Header，是因为它也参与签名计算。攻击者如果把 ",[17,138,123],{"code":123}," 或 ",[17,141,127],{"code":127}," 改掉，原签名就会失效。不过接收方仍然应该维护算法白名单，不能仅根据 Header 的内容决定是否接受某种算法。",[62,144,146],{"id":145},"payload真正要传递的业务数据","Payload：真正要传递的业务数据",[10,148,149,150,153],{},"Payload 是发送方希望传递的数据。本例将 ",[17,151,152],{"code":152},"Payload"," 结构体序列化为下面的 JSON：",[45,155,157],{"className":71,"code":156,"language":73,"meta":51,"style":51},"{\n  \"uid\": 1008676840,\n  \"session_id\": \"27a5d73b-11be-4d97-a728-2d08e9a25771\",\n  \"pkg\": \"com.codepzj.app.android\",\n  \"cou\": \"CN\",\n  \"did\": \"1102bc81-6f25-4d08-8f8d-46bba8c62d62\"\n}\n",[17,158,159,163,178,192,207,222,235],{"__ignoreMap":51},[77,160,161],{"class":79,"data-line":80,"line":80},[77,162,84],{"class":83},[77,164,165,167,170,172,175],{"class":79,"data-line":87,"line":87},[77,166,91],{"class":90},[77,168,169],{"class":94},"\"uid\"",[77,171,99],{"class":98},[77,173,174],{"class":94},"1008676840",[77,176,177],{"class":98},",\n",[77,179,180,182,185,187,190],{"class":79,"data-line":106,"line":106},[77,181,91],{"class":90},[77,183,184],{"class":94},"\"session_id\"",[77,186,99],{"class":98},[77,188,189],{"class":102},"\"27a5d73b-11be-4d97-a728-2d08e9a25771\"",[77,191,177],{"class":98},[77,193,195,197,200,202,205],{"class":79,"data-line":194,"line":194},4,[77,196,91],{"class":90},[77,198,199],{"class":94},"\"pkg\"",[77,201,99],{"class":98},[77,203,204],{"class":102},"\"com.codepzj.app.android\"",[77,206,177],{"class":98},[77,208,210,212,215,217,220],{"class":79,"data-line":209,"line":209},5,[77,211,91],{"class":90},[77,213,214],{"class":94},"\"cou\"",[77,216,99],{"class":98},[77,218,219],{"class":102},"\"CN\"",[77,221,177],{"class":98},[77,223,225,227,230,232],{"class":79,"data-line":224,"line":224},6,[77,226,91],{"class":90},[77,228,229],{"class":94},"\"did\"",[77,231,99],{"class":98},[77,233,234],{"class":102},"\"1102bc81-6f25-4d08-8f8d-46bba8c62d62\"\n",[77,236,238],{"class":79,"data-line":237,"line":237},7,[77,239,109],{"class":83},[10,241,242],{},"这段 JSON 经过 Base64URL 编码后成为第二部分。Base64URL 只是为了让二进制数据能安全地放进 URL 或 HTTP Header，它不是加密：拿到 JWS 的人无需密钥就能解码并看到 Payload。",[62,244,246],{"id":245},"signature证明前两部分没有被篡改","Signature：证明前两部分没有被篡改",[10,248,249],{},"签名方先把前两部分的编码结果用一个点号连接起来：",[45,251,254],{"className":252,"code":253,"language":50,"meta":51},[48],"BASE64URL(Protected Header) + \".\" + BASE64URL(Payload)\n",[17,255,253],{"__ignoreMap":51},[10,257,258,259,262],{},"这段内容称为 ",[133,260,261],{},"Signing Input","。随后使用 RS256 和 RSA 私钥对 Signing Input 计算数字签名，再将签名结果进行 Base64URL 编码，得到 JWS 的第三部分：",[45,264,267],{"className":265,"code":266,"language":50,"meta":51},[48],"Signature = RS256_SIGN(私钥, Signing Input)\n",[17,268,266],{"__ignoreMap":51},[10,270,271],{},"最终传输的完整 JWS 就是：",[45,273,276],{"className":274,"code":275,"language":50,"meta":51},[48],"Protected Header.Payload.Signature\n",[17,277,275],{"__ignoreMap":51},[10,279,280],{},"接收方收到 JWS 后，用点号拆出三部分，再使用公钥验证第三部分是否确实是前两部分的签名：",[45,282,285],{"className":283,"code":284,"language":50,"meta":51},[48],"RS256_VERIFY(公钥, Signing Input, Signature)\n",[17,286,284],{"__ignoreMap":51},[10,288,289],{},"验证成功可以证明两件事：Header 和 Payload 自签名生成后没有被修改；签名来自持有对应 RSA 私钥的一方。验证失败则说明内容被篡改、签名使用了错误私钥，或者验证方拿错了公钥。验证成功并不能证明 Payload 内容本身符合业务规则，过期时间、签发方和权限等仍需另外校验。",[10,291,292],{},"把上面的过程连起来，就是一条完整的签名与验签链路：",[294,295,296,299,302,305,308,311,314,317],"timeline",{},[10,297,298],{},"{1. 准备数据}",[10,300,301],{},"将 Protected Header 和 Payload 分别进行 Base64URL 编码。",[10,303,304],{},"{2. 构造 Signing Input}",[10,306,307],{},"用点号连接编码后的 Header 与 Payload。",[10,309,310],{},"{3. 生成签名}",[10,312,313],{},"签名方使用 RSA 私钥和 RS256 对 Signing Input 签名，组装成三段 JWS。",[10,315,316],{},"{4. 验证签名}",[10,318,319],{},"接收方使用 RSA 公钥验证 Signature，成功后才能信任 Payload 未被篡改。",[37,321,323],{"id":322},"jwtjwsjwe-与-jwk-的区别","JWT、JWS、JWE 与 JWK 的区别",[10,325,326],{},"这几个名称都属于 JOSE（JSON Object Signing and Encryption）规范体系，但它们解决的问题不同。",[328,329,330],"card-list",{},[331,332,333,340,346,352],"ul",{},[334,335,336,339],"li",{},[133,337,338],{},"JWT 定义传递什么数据","：用 Claims 描述用户、签发方、有效期等信息。",[334,341,342,345],{},[133,343,344],{},"JWS 定义如何签名数据","：防止数据被篡改，并验证数据来自谁。",[334,347,348,351],{},[133,349,350],{},"JWE 定义如何加密数据","：防止无关方看到数据，同时校验密文完整性。",[334,353,354,357],{},[133,355,356],{},"JWK 定义如何描述密钥","：用 JSON 表示 RSA、EC 等公钥或私钥。",[359,360,361,383],"table",{},[362,363,364],"thead",{},[365,366,367,371,374,377,380],"tr",{},[368,369,370],"th",{},"名称",[368,372,373],{},"全称",[368,375,376],{},"核心作用",[368,378,379],{},"是否隐藏内容",[368,381,382],{},"紧凑格式",[384,385,386,404,421,438],"tbody",{},[365,387,388,392,395,398,401],{},[389,390,391],"td",{},"JWT",[389,393,394],{},"JSON Web Token",[389,396,397],{},"定义一组可在系统间传递的 Claims",[389,399,400],{},"取决于使用 JWS 还是 JWE",[389,402,403],{},"通常是 3 段或 5 段",[365,405,406,409,412,415,418],{},[389,407,408],{},"JWS",[389,410,411],{},"JSON Web Signature",[389,413,414],{},"对任意载荷签名，保证完整性并验证来源",[389,416,417],{},"否",[389,419,420],{},"3 段",[365,422,423,426,429,432,435],{},[389,424,425],{},"JWE",[389,427,428],{},"JSON Web Encryption",[389,430,431],{},"对任意明文进行认证加密",[389,433,434],{},"是",[389,436,437],{},"5 段",[365,439,440,443,446,449,452],{},[389,441,442],{},"JWK",[389,444,445],{},"JSON Web Key",[389,447,448],{},"用 JSON 表示密码学密钥",[389,450,451],{},"不适用，它不是 Token",[389,453,454],{},"JSON 对象，没有点号分段",[10,456,457],{},"它们不是四种相互竞争的 Token。JWT 是数据及 Claims 规则，JWS 和 JWE 是保护、封装数据的方式，JWK 则负责表达过程中使用的密钥。",[62,459,461],{"id":460},"jwt定义-claims而不是一种签名算法","JWT：定义 Claims，而不是一种签名算法",[10,463,464],{},"JWT 的 Payload 是一个 Claims JSON 对象，例如：",[45,466,469],{"className":71,"code":467,"filename":468,"language":73,"meta":51,"style":51},"{\n  \"sub\": \"1008676840\",\n  \"iss\": \"https:\u002F\u002Fauth.example.com\",\n  \"aud\": \"com.codepzj.app.android\",\n  \"iat\": 1786550400,\n  \"exp\": 1786554000\n}\n","JWT Claims",[17,470,471,475,489,503,516,530,542],{"__ignoreMap":51},[77,472,473],{"class":79,"data-line":80,"line":80},[77,474,84],{"class":83},[77,476,477,479,482,484,487],{"class":79,"data-line":87,"line":87},[77,478,91],{"class":90},[77,480,481],{"class":94},"\"sub\"",[77,483,99],{"class":98},[77,485,486],{"class":102},"\"1008676840\"",[77,488,177],{"class":98},[77,490,491,493,496,498,501],{"class":79,"data-line":106,"line":106},[77,492,91],{"class":90},[77,494,495],{"class":94},"\"iss\"",[77,497,99],{"class":98},[77,499,500],{"class":102},"\"https:\u002F\u002Fauth.example.com\"",[77,502,177],{"class":98},[77,504,505,507,510,512,514],{"class":79,"data-line":194,"line":194},[77,506,91],{"class":90},[77,508,509],{"class":94},"\"aud\"",[77,511,99],{"class":98},[77,513,204],{"class":102},[77,515,177],{"class":98},[77,517,518,520,523,525,528],{"class":79,"data-line":209,"line":209},[77,519,91],{"class":90},[77,521,522],{"class":94},"\"iat\"",[77,524,99],{"class":98},[77,526,527],{"class":94},"1786550400",[77,529,177],{"class":98},[77,531,532,534,537,539],{"class":79,"data-line":224,"line":224},[77,533,91],{"class":90},[77,535,536],{"class":94},"\"exp\"",[77,538,99],{"class":98},[77,540,541],{"class":94},"1786554000\n",[77,543,544],{"class":79,"data-line":237,"line":237},[77,545,109],{"class":83},[547,548,550,553],"folding",{"title":549},"常见 JWT 注册 Claims",[10,551,552],{},"常见的注册 Claims 包括：",[331,554,555,561,567,573,579,585,591],{},[334,556,557,560],{},[17,558,559],{"code":559},"sub","（Subject）：这个 Token 描述的主体，通常是用户 ID。",[334,562,563,566],{},[17,564,565],{"code":565},"iss","（Issuer）：由谁签发。",[334,568,569,572],{},[17,570,571],{"code":571},"aud","（Audience）：允许哪个系统使用。",[334,574,575,578],{},[17,576,577],{"code":577},"iat","（Issued At）：签发时间。",[334,580,581,584],{},[17,582,583],{"code":583},"nbf","（Not Before）：在什么时间之后才能使用。",[334,586,587,590],{},[17,588,589],{"code":589},"exp","（Expiration Time）：过期时间。",[334,592,593,596],{},[17,594,595],{"code":595},"jti","（JWT ID）：Token 的唯一编号，可辅助防重放或吊销。",[10,598,599],{},"JWT 只规定 Claims 的表达和处理规则，并没有规定一定使用 RS256，也没有规定一定是“可读的三段字符串”。JWT 可以被 JWS 签名，也可以被 JWE 加密。",[10,601,602,603,606],{},"日常开发中所说的“JWT Token”通常是 ",[133,604,605],{},"Signed JWT","：把 JWT Claims 作为 JWS 的 Payload，因此呈现为三段结构：",[45,608,611],{"className":609,"code":610,"language":50,"meta":51},[48],"BASE64URL(Header).BASE64URL(JWT Claims).BASE64URL(Signature)\n",[17,612,610],{"__ignoreMap":51},[10,614,615,616,618,619,618,621,623],{},"这种 JWT 能验签，但 Payload 仍然可以被任何人解码查看。本文示例虽然也对 JSON 数据进行了 JWS 签名，但没有使用 JWT 包校验 ",[17,617,589],{"code":589},"、",[17,620,565],{"code":565},[17,622,571],{"code":571}," 等 Claims，所以更准确地说它是“JSON Payload 的 JWS 示例”。",[62,625,627],{"id":626},"jws签名保护不负责保密","JWS：签名保护，不负责保密",[10,629,630],{},"JWS 可以给任意字节数据签名，Payload 不一定是 JWT Claims，也不一定是 JSON。它解决的是：",[331,632,633,636],{},[334,634,635],{},"数据是否在传输过程中被修改；",[334,637,638],{},"数据是否由持有签名密钥的一方发出。",[10,640,641],{},"JWS 紧凑格式固定为三段：",[45,643,645],{"className":644,"code":275,"language":50,"meta":51},[48],[17,646,275],{"__ignoreMap":51},[10,648,649],{},"使用 RS256 时，签名方持有 RSA 私钥，验证方持有 RSA 公钥。公钥只能验证，不能生成对应签名，因此可以安全地分发给多个验证服务。",[62,651,653],{"id":652},"jwe加密保护紧凑格式有五段","JWE：加密保护，紧凑格式有五段",[10,655,656],{},"当 Payload 中包含不希望客户端、中间代理或日志系统看到的数据时，可以使用 JWE。JWE 的紧凑格式由五部分组成：",[45,658,661],{"className":659,"code":660,"language":50,"meta":51},[48],"Protected Header.Encrypted Key.IV.Ciphertext.Authentication Tag\n",[17,662,660],{"__ignoreMap":51},[10,664,665],{},"各部分含义如下：",[331,667,668,681,687,693,699],{},[334,669,670,673,674,676,677,680],{},[17,671,672],{"code":672},"Protected Header","：声明密钥管理算法 ",[17,675,123],{"code":123}," 和内容加密算法 ",[17,678,679],{"code":679},"enc","。",[334,682,683,686],{},[17,684,685],{"code":685},"Encrypted Key","：经过接收方密钥保护的内容加密密钥。",[334,688,689,692],{},[17,690,691],{"code":691},"IV","：内容加密算法使用的初始化向量。",[334,694,695,698],{},[17,696,697],{"code":697},"Ciphertext","：明文加密后的密文。",[334,700,701,704],{},[17,702,703],{"code":703},"Authentication Tag","：用于发现 Header 或密文是否被篡改的认证标签。",[10,706,707,708,710,711,713],{},"JWE Header 中的 ",[17,709,123],{"code":123}," 和 ",[17,712,679],{"code":679}," 含义不同。例如：",[45,715,718],{"className":71,"code":716,"filename":717,"language":73,"meta":51,"style":51},"{\n  \"alg\": \"RSA-OAEP-256\",\n  \"enc\": \"A256GCM\"\n}\n","JWE Protected Header",[17,719,720,724,737,749],{"__ignoreMap":51},[77,721,722],{"class":79,"data-line":80,"line":80},[77,723,84],{"class":83},[77,725,726,728,730,732,735],{"class":79,"data-line":87,"line":87},[77,727,91],{"class":90},[77,729,95],{"class":94},[77,731,99],{"class":98},[77,733,734],{"class":102},"\"RSA-OAEP-256\"",[77,736,177],{"class":98},[77,738,739,741,744,746],{"class":79,"data-line":106,"line":106},[77,740,91],{"class":90},[77,742,743],{"class":94},"\"enc\"",[77,745,99],{"class":98},[77,747,748],{"class":102},"\"A256GCM\"\n",[77,750,751],{"class":79,"data-line":194,"line":194},[77,752,109],{"class":83},[331,754,755,760],{},[334,756,757,759],{},[17,758,123],{"code":123}," 表示如何管理或传递内容加密密钥。本例使用接收方 RSA 公钥，通过 RSA-OAEP-256 保护这个密钥。",[334,761,762,764],{},[17,763,679],{"code":679}," 表示如何加密真正的 Payload。本例使用 A256GCM 对内容进行认证加密。",[10,766,767],{},"之所以不直接用 RSA 加密整个 Payload，是因为 RSA 不适合加密较长数据。JWE 通常采用混合加密：随机生成一个对称密钥加密正文，再使用接收方的公钥保护这个对称密钥。接收方使用私钥解出对称密钥后，才能还原正文。",[45,769,772],{"className":770,"code":771,"language":50,"meta":51},[48],"发送方：接收方公钥 + 明文 → JWE\n接收方：接收方私钥 + JWE → 明文\n",[17,773,771],{"__ignoreMap":51},[10,775,776],{},"JWE 不只是“把内容变得不可读”，其认证加密机制还会检测密文是否被篡改。",[62,778,780],{"id":779},"jwk用-json-表示密钥","JWK：用 JSON 表示密钥",[10,782,783],{},"PEM 和 JWK 都可以表示密钥，只是编码形式不同。本文从 PEM 文件读取 RSA 公钥，而同一个公钥也可以写成 JWK：",[45,785,788],{"className":71,"code":786,"filename":787,"language":73,"meta":51,"style":51},"{\n  \"kty\": \"RSA\",\n  \"kid\": \"rsa-key-2026-08\",\n  \"use\": \"sig\",\n  \"alg\": \"RS256\",\n  \"n\": \"0z8zW...省略...\",\n  \"e\": \"AQAB\"\n}\n","RSA Public JWK",[17,789,790,794,808,822,836,849,863,875],{"__ignoreMap":51},[77,791,792],{"class":79,"data-line":80,"line":80},[77,793,84],{"class":83},[77,795,796,798,801,803,806],{"class":79,"data-line":87,"line":87},[77,797,91],{"class":90},[77,799,800],{"class":94},"\"kty\"",[77,802,99],{"class":98},[77,804,805],{"class":102},"\"RSA\"",[77,807,177],{"class":98},[77,809,810,812,815,817,820],{"class":79,"data-line":106,"line":106},[77,811,91],{"class":90},[77,813,814],{"class":94},"\"kid\"",[77,816,99],{"class":98},[77,818,819],{"class":102},"\"rsa-key-2026-08\"",[77,821,177],{"class":98},[77,823,824,826,829,831,834],{"class":79,"data-line":194,"line":194},[77,825,91],{"class":90},[77,827,828],{"class":94},"\"use\"",[77,830,99],{"class":98},[77,832,833],{"class":102},"\"sig\"",[77,835,177],{"class":98},[77,837,838,840,842,844,847],{"class":79,"data-line":209,"line":209},[77,839,91],{"class":90},[77,841,95],{"class":94},[77,843,99],{"class":98},[77,845,846],{"class":102},"\"RS256\"",[77,848,177],{"class":98},[77,850,851,853,856,858,861],{"class":79,"data-line":224,"line":224},[77,852,91],{"class":90},[77,854,855],{"class":94},"\"n\"",[77,857,99],{"class":98},[77,859,860],{"class":102},"\"0z8zW...省略...\"",[77,862,177],{"class":98},[77,864,865,867,870,872],{"class":79,"data-line":237,"line":237},[77,866,91],{"class":90},[77,868,869],{"class":94},"\"e\"",[77,871,99],{"class":98},[77,873,874],{"class":102},"\"AQAB\"\n",[77,876,878],{"class":79,"data-line":877,"line":877},8,[77,879,109],{"class":83},[10,881,882],{},"常见字段包括：",[331,884,885,900,905,918,926],{},[334,886,887,890,891,618,894,139,897,680],{},[17,888,889],{"code":889},"kty","（Key Type）：密钥类型，例如 ",[17,892,893],{"code":893},"RSA",[17,895,896],{"code":896},"EC",[17,898,899],{"code":899},"OKP",[334,901,902,904],{},[17,903,127],{"code":127},"（Key ID）：密钥编号，用于从多个候选密钥中选择一个。",[334,906,907,910,911,914,915,917],{},[17,908,909],{"code":909},"use","（Public Key Use）：用途，",[17,912,913],{"code":913},"sig"," 表示签名，",[17,916,679],{"code":679}," 表示加密。",[334,919,920,922,923,680],{},[17,921,123],{"code":123},"：建议搭配使用的算法，例如 ",[17,924,925],{"code":925},"RS256",[334,927,928,618,931,934],{},[17,929,930],{"code":930},"n",[17,932,933],{"code":933},"e","：RSA 公钥的模数和公开指数，使用 Base64URL 编码。",[10,936,937,938,618,941,618,943,946],{},"如果 JWK 中出现 RSA 私钥参数 ",[17,939,940],{"code":940},"d",[17,942,10],{"code":10},[17,944,945],{"code":945},"q"," 等，它就是私钥 JWK，必须像 PEM 私钥一样严格保护，不能通过公开接口下发。",[10,948,949],{},"多个 JWK 可以组成 JWKS（JSON Web Key Set）：",[45,951,954],{"className":71,"code":952,"filename":953,"language":73,"meta":51,"style":51},"{\n  \"keys\": [\n    {\n      \"kty\": \"RSA\",\n      \"kid\": \"rsa-key-2026-08\",\n      \"use\": \"sig\",\n      \"alg\": \"RS256\",\n      \"n\": \"0z8zW...省略...\",\n      \"e\": \"AQAB\"\n    }\n  ]\n}\n","JWKS",[17,955,956,960,973,984,1000,1016,1032,1048,1064,1079,1090,1100],{"__ignoreMap":51},[77,957,958],{"class":79,"data-line":80,"line":80},[77,959,84],{"class":83},[77,961,962,964,967,969],{"class":79,"data-line":87,"line":87},[77,963,91],{"class":90},[77,965,966],{"class":94},"\"keys\"",[77,968,99],{"class":98},[77,970,972],{"class":971},"sJX2D","[\n",[77,974,975,977,979,981],{"class":79,"data-line":106,"line":106},[77,976,91],{"class":90},[77,978,91],{"class":90},[77,980,51],{"class":98},[77,982,84],{"class":983},"sqkie",[77,985,986,988,990,992,994,996,998],{"class":79,"data-line":194,"line":194},[77,987,91],{"class":90},[77,989,91],{"class":90},[77,991,91],{"class":90},[77,993,800],{"class":94},[77,995,99],{"class":98},[77,997,805],{"class":102},[77,999,177],{"class":98},[77,1001,1002,1004,1006,1008,1010,1012,1014],{"class":79,"data-line":209,"line":209},[77,1003,91],{"class":90},[77,1005,91],{"class":90},[77,1007,91],{"class":90},[77,1009,814],{"class":94},[77,1011,99],{"class":98},[77,1013,819],{"class":102},[77,1015,177],{"class":98},[77,1017,1018,1020,1022,1024,1026,1028,1030],{"class":79,"data-line":224,"line":224},[77,1019,91],{"class":90},[77,1021,91],{"class":90},[77,1023,91],{"class":90},[77,1025,828],{"class":94},[77,1027,99],{"class":98},[77,1029,833],{"class":102},[77,1031,177],{"class":98},[77,1033,1034,1036,1038,1040,1042,1044,1046],{"class":79,"data-line":237,"line":237},[77,1035,91],{"class":90},[77,1037,91],{"class":90},[77,1039,91],{"class":90},[77,1041,95],{"class":94},[77,1043,99],{"class":98},[77,1045,846],{"class":102},[77,1047,177],{"class":98},[77,1049,1050,1052,1054,1056,1058,1060,1062],{"class":79,"data-line":877,"line":877},[77,1051,91],{"class":90},[77,1053,91],{"class":90},[77,1055,91],{"class":90},[77,1057,855],{"class":94},[77,1059,99],{"class":98},[77,1061,860],{"class":102},[77,1063,177],{"class":98},[77,1065,1067,1069,1071,1073,1075,1077],{"class":79,"data-line":1066,"line":1066},9,[77,1068,91],{"class":90},[77,1070,91],{"class":90},[77,1072,91],{"class":90},[77,1074,869],{"class":94},[77,1076,99],{"class":98},[77,1078,874],{"class":102},[77,1080,1082,1084,1086,1088],{"class":79,"data-line":1081,"line":1081},10,[77,1083,91],{"class":90},[77,1085,91],{"class":90},[77,1087,51],{"class":98},[77,1089,109],{"class":983},[77,1091,1093,1095,1097],{"class":79,"data-line":1092,"line":1092},11,[77,1094,91],{"class":90},[77,1096,51],{"class":98},[77,1098,1099],{"class":971},"]\n",[77,1101,1103],{"class":79,"data-line":1102,"line":1102},12,[77,1104,109],{"class":83},[10,1106,1107,1108,1110,1111,1113],{},"身份服务通常公开一个 JWKS 地址，只发布公钥。验证方读取 JWS Header 中的 ",[17,1109,127],{"code":127},"，再从 JWKS 找到相同 ",[17,1112,127],{"code":127}," 的公钥完成验签。这样可以在不中断服务的情况下同时保留新旧公钥，实现密钥轮换。",[45,1115,1118],{"className":1116,"code":1117,"language":50,"meta":51},[48],"JWS Header 中的 kid\n        ↓ 匹配\nJWKS 中某个 JWK\n        ↓ 取出公钥\n验证 JWS Signature\n",[17,1119,1117],{"__ignoreMap":51},[62,1121,1123],{"id":1122},"签名-jwt加密-jwt-与嵌套-jwt","签名 JWT、加密 JWT 与嵌套 JWT",[10,1125,1126],{},"把这些概念组合起来，常见的 Token 有三种形态：",[1128,1129,1131,1146,1160],"tab",{":tabs":1130},"[\"签名 JWT\", \"加密 JWT\", \"嵌套 JWT\"]",[1132,1133,1134,1137,1143],"template",{"v-slot:tab1":51},[10,1135,1136],{},"JWT Claims 作为 JWS Payload：",[45,1138,1141],{"className":1139,"code":1140,"language":50,"meta":51},[48],"JWT Claims → JWS\n",[17,1142,1140],{"__ignoreMap":51},[10,1144,1145],{},"最终得到三段 Token。内容可以直接解码查看，但接收方能够发现篡改并验证签发来源。这是日常鉴权中最常见的 JWT 形态。",[1132,1147,1148,1151,1157],{"v-slot:tab2":51},[10,1149,1150],{},"JWT Claims 作为 JWE 明文：",[45,1152,1155],{"className":1153,"code":1154,"language":50,"meta":51},[48],"JWT Claims → JWE\n",[17,1156,1154],{"__ignoreMap":51},[10,1158,1159],{},"最终得到五段 Token。只有持有解密密钥的接收方能够读取 Claims，适合 Payload 必须保密的场景。",[1132,1161,1162,1165,1171],{"v-slot:tab3":51},[10,1163,1164],{},"先将 JWT Claims 签名为 JWS，再加密整个 JWS：",[45,1166,1169],{"className":1167,"code":1168,"language":50,"meta":51},[48],"JWT Claims → JWS → JWE\n",[17,1170,1168],{"__ignoreMap":51},[10,1172,1173],{},"接收方先解密外层 JWE，再验证内层 JWS，同时获得保密性、完整性和来源认证。",[10,1175,1176,1177,1180],{},"在嵌套 JWT 中，外层 JWE Header 通常会使用 ",[17,1178,1179],{"code":1179},"cty: \"JWT\""," 表明解密后得到的内容仍然是一个 JWT。",[10,1182,1183],{},"如何选择取决于安全目标：只需要防篡改和验证来源时使用签名 JWT；Payload 含有必须保密的数据时使用 JWE；既要隐藏内容，又要让最终接收方验证原始签发者时，可以使用嵌套 JWT。无论选择哪种形式，业务层仍然必须校验过期时间、签发方、接收方和权限。",[37,1185,1187],{"id":1186},"本文为何使用-jws-与-rs256","本文为何使用 JWS 与 RS256",[10,1189,1190,1191,1193],{},"同样需要注意，JWS 并不限定只能使用非对称算法。它既可以使用 HS256 这类共享密钥算法，也可以使用 RS256、ES256 等非对称算法。本文选择的 ",[133,1192,925],{}," 表示：",[331,1195,1196,1199,1202],{},[334,1197,1198],{},"使用 RSA 密钥对；",[334,1200,1201],{},"使用 SHA-256 计算摘要；",[334,1203,1204],{},"使用 RSASSA-PKCS1-v1_5 生成和验证签名。",[10,1206,1207],{},"RS256 的优势在于签名方只需保管私钥，验证方只需持有公钥。验证服务可以判断签名是否合法，但无法使用公钥伪造新的签名，适合多个系统只负责验签的场景。",[22,1209,1212],{"title":1210,"type":1211},"JWS 只保证完整性和来源可信","warning",[10,1213,1214],{},"JWS 的 Payload 只是经过 Base64URL 编码，并没有被加密。任何拿到 JWS 的人都可以解码并查看载荷，因此不要把密码、私钥等敏感信息直接放入 Payload。需要隐藏内容时，应使用 JWE 或在业务层单独加密。",[37,1216,1217],{"id":1217},"初始化项目",[10,1219,1220,1221,1224],{},"示例使用 ",[17,1222,1223],{"code":1223},"lestrrat-go\u002Fjwx"," v4：",[45,1226,1230],{"className":1227,"code":1228,"language":1229,"meta":51,"style":51},"language-bash shiki shiki-themes github-light github-dark","go get github.com\u002Flestrrat-go\u002Fjwx\u002Fv4\ngo get github.com\u002Fgoogle\u002Fuuid\n","bash",[17,1231,1232,1244],{"__ignoreMap":51},[77,1233,1234,1238,1241],{"class":79,"data-line":80,"line":80},[77,1235,1237],{"class":1236},"sScJk","go",[77,1239,1240],{"class":102}," get",[77,1242,1243],{"class":102}," github.com\u002Flestrrat-go\u002Fjwx\u002Fv4\n",[77,1245,1246,1248,1250],{"class":79,"data-line":87,"line":87},[77,1247,1237],{"class":1236},[77,1249,1240],{"class":102},[77,1251,1252],{"class":102}," github.com\u002Fgoogle\u002Fuuid\n",[10,1254,1255],{},"目录结构如下：",[45,1257,1260],{"className":1258,"code":1259,"language":50,"meta":51},[48],"jws\u002F\n├── main.go\n└── rsa\u002F\n    ├── private.pem\n    └── public.pem\n",[17,1261,1259],{"__ignoreMap":51},[37,1263,1265],{"id":1264},"使用-openssl-生成-rsa-密钥对","使用 OpenSSL 生成 RSA 密钥对",[10,1267,1268],{},"先进入示例目录并创建密钥目录：",[45,1270,1272],{"className":1227,"code":1271,"language":1229,"meta":51,"style":51},"mkdir -p rsa\n",[17,1273,1274],{"__ignoreMap":51},[77,1275,1276,1279,1282],{"class":79,"data-line":80,"line":80},[77,1277,1278],{"class":1236},"mkdir",[77,1280,1281],{"class":94}," -p",[77,1283,1284],{"class":102}," rsa\n",[10,1286,1287],{},"生成一个 2048 位 RSA 私钥：",[45,1289,1291],{"className":1227,"code":1290,"language":1229,"meta":51,"style":51},"openssl genrsa -out rsa\u002Fprivate.pem 2048\n",[17,1292,1293],{"__ignoreMap":51},[77,1294,1295,1298,1301,1304,1307],{"class":79,"data-line":80,"line":80},[77,1296,1297],{"class":1236},"openssl",[77,1299,1300],{"class":102}," genrsa",[77,1302,1303],{"class":94}," -out",[77,1305,1306],{"class":102}," rsa\u002Fprivate.pem",[77,1308,1309],{"class":94}," 2048\n",[10,1311,1312],{},"再从私钥中导出公钥：",[45,1314,1316],{"className":1227,"code":1315,"language":1229,"meta":51,"style":51},"openssl rsa -in rsa\u002Fprivate.pem -pubout -out rsa\u002Fpublic.pem\n",[17,1317,1318],{"__ignoreMap":51},[77,1319,1320,1322,1325,1328,1330,1333,1335],{"class":79,"data-line":80,"line":80},[77,1321,1297],{"class":1236},[77,1323,1324],{"class":102}," rsa",[77,1326,1327],{"class":94}," -in",[77,1329,1306],{"class":102},[77,1331,1332],{"class":94}," -pubout",[77,1334,1303],{"class":94},[77,1336,1337],{"class":102}," rsa\u002Fpublic.pem\n",[10,1339,1340],{},"生成后可以检查文件头：",[45,1342,1344],{"className":1227,"code":1343,"language":1229,"meta":51,"style":51},"head -n 1 rsa\u002Fprivate.pem\nhead -n 1 rsa\u002Fpublic.pem\n",[17,1345,1346,1360],{"__ignoreMap":51},[77,1347,1348,1351,1354,1357],{"class":79,"data-line":80,"line":80},[77,1349,1350],{"class":1236},"head",[77,1352,1353],{"class":94}," -n",[77,1355,1356],{"class":94}," 1",[77,1358,1359],{"class":102}," rsa\u002Fprivate.pem\n",[77,1361,1362,1364,1366,1368],{"class":79,"data-line":87,"line":87},[77,1363,1350],{"class":1236},[77,1365,1353],{"class":94},[77,1367,1356],{"class":94},[77,1369,1337],{"class":102},[10,1371,1372],{},"本文示例使用的文件头为：",[45,1374,1377],{"className":1375,"code":1376,"language":50,"meta":51},[48],"-----BEGIN PRIVATE KEY-----\n-----BEGIN PUBLIC KEY-----\n",[17,1378,1376],{"__ignoreMap":51},[10,1380,1381,1382,1385,1386,1389],{},"其中，",[17,1383,1384],{"code":1384},"PRIVATE KEY"," 对应 PKCS#8 私钥，",[17,1387,1388],{"code":1388},"PUBLIC KEY"," 对应 X.509 SubjectPublicKeyInfo 公钥。后续 Go 代码需要选择与文件格式匹配的解析函数。",[22,1391,1393],{"title":1392,"type":25},"不同 OpenSSL 版本可能生成不同格式",[10,1394,1395,1396,1399,1400,1403,1404,680],{},"如果私钥文件头是 ",[17,1397,1398],{"code":1398},"-----BEGIN RSA PRIVATE KEY-----","，它通常是 PKCS#1 格式，应使用 ",[17,1401,1402],{"code":1402},"x509.ParsePKCS1PrivateKey"," 解析，不能直接传给 ",[17,1405,1406],{"code":1406},"x509.ParsePKCS8PrivateKey",[10,1408,1409],{},"私钥必须严格限制访问权限，也不应该提交到 Git 仓库：",[45,1411,1413],{"className":1227,"code":1412,"language":1229,"meta":51,"style":51},"chmod 600 rsa\u002Fprivate.pem\n",[17,1414,1415],{"__ignoreMap":51},[77,1416,1417,1420,1423],{"class":79,"data-line":80,"line":80},[77,1418,1419],{"class":1236},"chmod",[77,1421,1422],{"class":94}," 600",[77,1424,1359],{"class":102},[10,1426,1427],{},"生产环境中建议通过密钥管理服务、Secret 或安全的配置中心注入私钥，而不是把它和应用代码一起分发。",[37,1429,1431],{"id":1430},"从-pem-文件读取-rsa-私钥","从 PEM 文件读取 RSA 私钥",[10,1433,1434,1435,1438],{},"磁盘中的密钥使用 PEM 文本格式保存，而 ",[17,1436,1437],{"code":1437},"crypto\u002Fx509"," 实际解析的是 PEM 内部的 DER 二进制数据。因此读取私钥需要经过以下步骤：",[1440,1441,1442,1449,1455,1460],"ol",{},[334,1443,1444,1445,1448],{},"使用 ",[17,1446,1447],{"code":1447},"os.ReadFile"," 读取文件；",[334,1450,1444,1451,1454],{},[17,1452,1453],{"code":1453},"pem.Decode"," 取出 DER 数据；",[334,1456,1444,1457,1459],{},[17,1458,1406],{"code":1406}," 解析 PKCS#8；",[334,1461,1462],{},"通过类型断言确认它是 RSA 私钥。",[45,1464,1467],{"className":1465,"code":1466,"language":1237,"meta":51,"style":51},"language-go shiki shiki-themes github-light github-dark","func readRSAPrivateKeyFromPath(path string) (*rsa.PrivateKey, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblock, _ := pem.Decode(data)\n\tif block == nil {\n\t\treturn nil, errors.New(\"failed to decode PEM\")\n\t}\n\n\tkey, err := x509.ParsePKCS8PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprivateKey, ok := key.(*rsa.PrivateKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"private key is not RSA\")\n\t}\n\treturn privateKey, nil\n}\n",[17,1468,1469,1519,1543,1563,1577,1585,1591,1613,1631,1654,1662,1666,1688,1705,1718,1727,1732,1757,1772,1794,1803,1816],{"__ignoreMap":51},[77,1470,1471,1475,1478,1481,1485,1488,1491,1494,1496,1499,1502,1504,1507,1510,1513,1515,1517],{"class":79,"data-line":80,"line":80},[77,1472,1474],{"class":1473},"szBVR","func",[77,1476,1477],{"class":1236}," readRSAPrivateKeyFromPath",[77,1479,1480],{"class":83},"(",[77,1482,1484],{"class":1483},"s4XuR","path",[77,1486,1487],{"class":1473}," string",[77,1489,1490],{"class":83},")",[77,1492,1493],{"class":98}," ",[77,1495,1480],{"class":83},[77,1497,1498],{"class":1473},"*",[77,1500,1501],{"class":1236},"rsa",[77,1503,59],{"class":98},[77,1505,1506],{"class":1236},"PrivateKey",[77,1508,1509],{"class":98},", ",[77,1511,1512],{"class":1473},"error",[77,1514,1490],{"class":83},[77,1516,1493],{"class":98},[77,1518,84],{"class":83},[77,1520,1521,1524,1527,1530,1533,1536,1538,1540],{"class":79,"data-line":87,"line":87},[77,1522,1523],{"class":90},"\t",[77,1525,1526],{"class":98},"data, err ",[77,1528,1529],{"class":1473},":=",[77,1531,1532],{"class":98}," os.",[77,1534,1535],{"class":1236},"ReadFile",[77,1537,1480],{"class":971},[77,1539,1484],{"class":98},[77,1541,1542],{"class":971},")\n",[77,1544,1545,1547,1550,1553,1556,1559,1561],{"class":79,"data-line":106,"line":106},[77,1546,1523],{"class":90},[77,1548,1549],{"class":1473},"if",[77,1551,1552],{"class":98}," err ",[77,1554,1555],{"class":1473},"!=",[77,1557,1558],{"class":94}," nil",[77,1560,1493],{"class":98},[77,1562,84],{"class":971},[77,1564,1565,1567,1569,1572,1574],{"class":79,"data-line":194,"line":194},[77,1566,1523],{"class":90},[77,1568,1523],{"class":90},[77,1570,1571],{"class":1473},"return",[77,1573,1558],{"class":94},[77,1575,1576],{"class":98},", err\n",[77,1578,1579,1581,1583],{"class":79,"data-line":209,"line":209},[77,1580,1523],{"class":90},[77,1582,51],{"class":98},[77,1584,109],{"class":971},[77,1586,1587],{"class":79,"data-line":224,"line":224},[77,1588,1590],{"emptyLinePlaceholder":1589},true,"\n",[77,1592,1593,1595,1598,1600,1603,1606,1608,1611],{"class":79,"data-line":237,"line":237},[77,1594,1523],{"class":90},[77,1596,1597],{"class":98},"block, _ ",[77,1599,1529],{"class":1473},[77,1601,1602],{"class":98}," pem.",[77,1604,1605],{"class":1236},"Decode",[77,1607,1480],{"class":971},[77,1609,1610],{"class":98},"data",[77,1612,1542],{"class":971},[77,1614,1615,1617,1619,1622,1625,1627,1629],{"class":79,"data-line":877,"line":877},[77,1616,1523],{"class":90},[77,1618,1549],{"class":1473},[77,1620,1621],{"class":98}," block ",[77,1623,1624],{"class":1473},"==",[77,1626,1558],{"class":94},[77,1628,1493],{"class":98},[77,1630,84],{"class":971},[77,1632,1633,1635,1637,1639,1641,1644,1647,1649,1652],{"class":79,"data-line":1066,"line":1066},[77,1634,1523],{"class":90},[77,1636,1523],{"class":90},[77,1638,1571],{"class":1473},[77,1640,1558],{"class":94},[77,1642,1643],{"class":98},", errors.",[77,1645,1646],{"class":1236},"New",[77,1648,1480],{"class":983},[77,1650,1651],{"class":102},"\"failed to decode PEM\"",[77,1653,1542],{"class":983},[77,1655,1656,1658,1660],{"class":79,"data-line":1081,"line":1081},[77,1657,1523],{"class":90},[77,1659,51],{"class":98},[77,1661,109],{"class":971},[77,1663,1664],{"class":79,"data-line":1092,"line":1092},[77,1665,1590],{"emptyLinePlaceholder":1589},[77,1667,1668,1670,1673,1675,1678,1681,1683,1686],{"class":79,"data-line":1102,"line":1102},[77,1669,1523],{"class":90},[77,1671,1672],{"class":98},"key, err ",[77,1674,1529],{"class":1473},[77,1676,1677],{"class":98}," x509.",[77,1679,1680],{"class":1236},"ParsePKCS8PrivateKey",[77,1682,1480],{"class":971},[77,1684,1685],{"class":98},"block.Bytes",[77,1687,1542],{"class":971},[77,1689,1691,1693,1695,1697,1699,1701,1703],{"class":79,"data-line":1690,"line":1690},13,[77,1692,1523],{"class":90},[77,1694,1549],{"class":1473},[77,1696,1552],{"class":98},[77,1698,1555],{"class":1473},[77,1700,1558],{"class":94},[77,1702,1493],{"class":98},[77,1704,84],{"class":971},[77,1706,1708,1710,1712,1714,1716],{"class":79,"data-line":1707,"line":1707},14,[77,1709,1523],{"class":90},[77,1711,1523],{"class":90},[77,1713,1571],{"class":1473},[77,1715,1558],{"class":94},[77,1717,1576],{"class":98},[77,1719,1721,1723,1725],{"class":79,"data-line":1720,"line":1720},15,[77,1722,1523],{"class":90},[77,1724,51],{"class":98},[77,1726,109],{"class":971},[77,1728,1730],{"class":79,"data-line":1729,"line":1729},16,[77,1731,1590],{"emptyLinePlaceholder":1589},[77,1733,1735,1737,1740,1742,1745,1747,1749,1751,1753,1755],{"class":79,"data-line":1734,"line":1734},17,[77,1736,1523],{"class":90},[77,1738,1739],{"class":98},"privateKey, ok ",[77,1741,1529],{"class":1473},[77,1743,1744],{"class":98}," key.",[77,1746,1480],{"class":971},[77,1748,1498],{"class":1473},[77,1750,1501],{"class":1236},[77,1752,59],{"class":98},[77,1754,1506],{"class":1236},[77,1756,1542],{"class":971},[77,1758,1760,1762,1764,1767,1770],{"class":79,"data-line":1759,"line":1759},18,[77,1761,1523],{"class":90},[77,1763,1549],{"class":1473},[77,1765,1766],{"class":1473}," !",[77,1768,1769],{"class":98},"ok ",[77,1771,84],{"class":971},[77,1773,1775,1777,1779,1781,1783,1785,1787,1789,1792],{"class":79,"data-line":1774,"line":1774},19,[77,1776,1523],{"class":90},[77,1778,1523],{"class":90},[77,1780,1571],{"class":1473},[77,1782,1558],{"class":94},[77,1784,1643],{"class":98},[77,1786,1646],{"class":1236},[77,1788,1480],{"class":983},[77,1790,1791],{"class":102},"\"private key is not RSA\"",[77,1793,1542],{"class":983},[77,1795,1797,1799,1801],{"class":79,"data-line":1796,"line":1796},20,[77,1798,1523],{"class":90},[77,1800,51],{"class":98},[77,1802,109],{"class":971},[77,1804,1806,1808,1810,1813],{"class":79,"data-line":1805,"line":1805},21,[77,1807,1523],{"class":90},[77,1809,1571],{"class":1473},[77,1811,1812],{"class":98}," privateKey, ",[77,1814,1815],{"class":94},"nil\n",[77,1817,1819],{"class":79,"data-line":1818,"line":1818},22,[77,1820,109],{"class":83},[10,1822,1823,1825,1826,1829,1830,680],{},[17,1824,1680],{"code":1680}," 的返回值是 ",[17,1827,1828],{"code":1828},"any","，因为 PKCS#8 可以保存不同类型的私钥，所以还要将结果断言为 ",[17,1831,1832],{"code":1832},"*rsa.PrivateKey",[37,1834,1836],{"id":1835},"从-pem-文件读取-rsa-公钥","从 PEM 文件读取 RSA 公钥",[10,1838,1839,1840,1842,1843,1846],{},"公钥的读取过程类似，不过 ",[17,1841,1388],{"code":1388}," 文件使用的是 PKIX 公钥格式，需要调用 ",[17,1844,1845],{"code":1845},"x509.ParsePKIXPublicKey","：",[45,1848,1850],{"className":1465,"code":1849,"language":1237,"meta":51,"style":51},"func readRSAPublicKeyFromPath(path string) (*rsa.PublicKey, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblock, _ := pem.Decode(data)\n\tif block == nil {\n\t\treturn nil, errors.New(\"failed to decode PEM\")\n\t}\n\n\tkey, err := x509.ParsePKIXPublicKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpublicKey, ok := key.(*rsa.PublicKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"public key is not RSA\")\n\t}\n\treturn publicKey, nil\n}\n",[17,1851,1852,1890,1908,1924,1936,1944,1948,1966,1982,2002,2010,2014,2033,2049,2061,2069,2073,2096,2108,2129,2137,2148],{"__ignoreMap":51},[77,1853,1854,1856,1859,1861,1863,1865,1867,1869,1871,1873,1875,1877,1880,1882,1884,1886,1888],{"class":79,"data-line":80,"line":80},[77,1855,1474],{"class":1473},[77,1857,1858],{"class":1236}," readRSAPublicKeyFromPath",[77,1860,1480],{"class":83},[77,1862,1484],{"class":1483},[77,1864,1487],{"class":1473},[77,1866,1490],{"class":83},[77,1868,1493],{"class":98},[77,1870,1480],{"class":83},[77,1872,1498],{"class":1473},[77,1874,1501],{"class":1236},[77,1876,59],{"class":98},[77,1878,1879],{"class":1236},"PublicKey",[77,1881,1509],{"class":98},[77,1883,1512],{"class":1473},[77,1885,1490],{"class":83},[77,1887,1493],{"class":98},[77,1889,84],{"class":83},[77,1891,1892,1894,1896,1898,1900,1902,1904,1906],{"class":79,"data-line":87,"line":87},[77,1893,1523],{"class":90},[77,1895,1526],{"class":98},[77,1897,1529],{"class":1473},[77,1899,1532],{"class":98},[77,1901,1535],{"class":1236},[77,1903,1480],{"class":971},[77,1905,1484],{"class":98},[77,1907,1542],{"class":971},[77,1909,1910,1912,1914,1916,1918,1920,1922],{"class":79,"data-line":106,"line":106},[77,1911,1523],{"class":90},[77,1913,1549],{"class":1473},[77,1915,1552],{"class":98},[77,1917,1555],{"class":1473},[77,1919,1558],{"class":94},[77,1921,1493],{"class":98},[77,1923,84],{"class":971},[77,1925,1926,1928,1930,1932,1934],{"class":79,"data-line":194,"line":194},[77,1927,1523],{"class":90},[77,1929,1523],{"class":90},[77,1931,1571],{"class":1473},[77,1933,1558],{"class":94},[77,1935,1576],{"class":98},[77,1937,1938,1940,1942],{"class":79,"data-line":209,"line":209},[77,1939,1523],{"class":90},[77,1941,51],{"class":98},[77,1943,109],{"class":971},[77,1945,1946],{"class":79,"data-line":224,"line":224},[77,1947,1590],{"emptyLinePlaceholder":1589},[77,1949,1950,1952,1954,1956,1958,1960,1962,1964],{"class":79,"data-line":237,"line":237},[77,1951,1523],{"class":90},[77,1953,1597],{"class":98},[77,1955,1529],{"class":1473},[77,1957,1602],{"class":98},[77,1959,1605],{"class":1236},[77,1961,1480],{"class":971},[77,1963,1610],{"class":98},[77,1965,1542],{"class":971},[77,1967,1968,1970,1972,1974,1976,1978,1980],{"class":79,"data-line":877,"line":877},[77,1969,1523],{"class":90},[77,1971,1549],{"class":1473},[77,1973,1621],{"class":98},[77,1975,1624],{"class":1473},[77,1977,1558],{"class":94},[77,1979,1493],{"class":98},[77,1981,84],{"class":971},[77,1983,1984,1986,1988,1990,1992,1994,1996,1998,2000],{"class":79,"data-line":1066,"line":1066},[77,1985,1523],{"class":90},[77,1987,1523],{"class":90},[77,1989,1571],{"class":1473},[77,1991,1558],{"class":94},[77,1993,1643],{"class":98},[77,1995,1646],{"class":1236},[77,1997,1480],{"class":983},[77,1999,1651],{"class":102},[77,2001,1542],{"class":983},[77,2003,2004,2006,2008],{"class":79,"data-line":1081,"line":1081},[77,2005,1523],{"class":90},[77,2007,51],{"class":98},[77,2009,109],{"class":971},[77,2011,2012],{"class":79,"data-line":1092,"line":1092},[77,2013,1590],{"emptyLinePlaceholder":1589},[77,2015,2016,2018,2020,2022,2024,2027,2029,2031],{"class":79,"data-line":1102,"line":1102},[77,2017,1523],{"class":90},[77,2019,1672],{"class":98},[77,2021,1529],{"class":1473},[77,2023,1677],{"class":98},[77,2025,2026],{"class":1236},"ParsePKIXPublicKey",[77,2028,1480],{"class":971},[77,2030,1685],{"class":98},[77,2032,1542],{"class":971},[77,2034,2035,2037,2039,2041,2043,2045,2047],{"class":79,"data-line":1690,"line":1690},[77,2036,1523],{"class":90},[77,2038,1549],{"class":1473},[77,2040,1552],{"class":98},[77,2042,1555],{"class":1473},[77,2044,1558],{"class":94},[77,2046,1493],{"class":98},[77,2048,84],{"class":971},[77,2050,2051,2053,2055,2057,2059],{"class":79,"data-line":1707,"line":1707},[77,2052,1523],{"class":90},[77,2054,1523],{"class":90},[77,2056,1571],{"class":1473},[77,2058,1558],{"class":94},[77,2060,1576],{"class":98},[77,2062,2063,2065,2067],{"class":79,"data-line":1720,"line":1720},[77,2064,1523],{"class":90},[77,2066,51],{"class":98},[77,2068,109],{"class":971},[77,2070,2071],{"class":79,"data-line":1729,"line":1729},[77,2072,1590],{"emptyLinePlaceholder":1589},[77,2074,2075,2077,2080,2082,2084,2086,2088,2090,2092,2094],{"class":79,"data-line":1734,"line":1734},[77,2076,1523],{"class":90},[77,2078,2079],{"class":98},"publicKey, ok ",[77,2081,1529],{"class":1473},[77,2083,1744],{"class":98},[77,2085,1480],{"class":971},[77,2087,1498],{"class":1473},[77,2089,1501],{"class":1236},[77,2091,59],{"class":98},[77,2093,1879],{"class":1236},[77,2095,1542],{"class":971},[77,2097,2098,2100,2102,2104,2106],{"class":79,"data-line":1759,"line":1759},[77,2099,1523],{"class":90},[77,2101,1549],{"class":1473},[77,2103,1766],{"class":1473},[77,2105,1769],{"class":98},[77,2107,84],{"class":971},[77,2109,2110,2112,2114,2116,2118,2120,2122,2124,2127],{"class":79,"data-line":1774,"line":1774},[77,2111,1523],{"class":90},[77,2113,1523],{"class":90},[77,2115,1571],{"class":1473},[77,2117,1558],{"class":94},[77,2119,1643],{"class":98},[77,2121,1646],{"class":1236},[77,2123,1480],{"class":983},[77,2125,2126],{"class":102},"\"public key is not RSA\"",[77,2128,1542],{"class":983},[77,2130,2131,2133,2135],{"class":79,"data-line":1796,"line":1796},[77,2132,1523],{"class":90},[77,2134,51],{"class":98},[77,2136,109],{"class":971},[77,2138,2139,2141,2143,2146],{"class":79,"data-line":1805,"line":1805},[77,2140,1523],{"class":90},[77,2142,1571],{"class":1473},[77,2144,2145],{"class":98}," publicKey, ",[77,2147,1815],{"class":94},[77,2149,2150],{"class":79,"data-line":1818,"line":1818},[77,2151,109],{"class":83},[10,2153,2154],{},"这里同样需要类型断言，因为 PKIX 格式还可能保存 ECDSA、Ed25519 等其他类型的公钥。",[37,2156,2158],{"id":2157},"定义需要签名的-payload","定义需要签名的 Payload",[10,2160,2161],{},"示例使用一个结构体表示业务载荷：",[45,2163,2165],{"className":1465,"code":2164,"language":1237,"meta":51,"style":51},"type Payload struct {\n\tUID       int64  `json:\"uid\"`\n\tSessionID string `json:\"session_id\"`\n\tPkg       string `json:\"pkg\"`\n\tCou       string `json:\"cou\"`\n\tDID       string `json:\"did\"`\n}\n",[17,2166,2167,2182,2195,2208,2220,2232,2244],{"__ignoreMap":51},[77,2168,2169,2172,2175,2178,2180],{"class":79,"data-line":80,"line":80},[77,2170,2171],{"class":1473},"type",[77,2173,2174],{"class":1236}," Payload",[77,2176,2177],{"class":1473}," struct",[77,2179,1493],{"class":98},[77,2181,84],{"class":83},[77,2183,2184,2186,2189,2192],{"class":79,"data-line":87,"line":87},[77,2185,1523],{"class":90},[77,2187,2188],{"class":98},"UID       ",[77,2190,2191],{"class":1473},"int64",[77,2193,2194],{"class":102},"  `json:\"uid\"`\n",[77,2196,2197,2199,2202,2205],{"class":79,"data-line":106,"line":106},[77,2198,1523],{"class":90},[77,2200,2201],{"class":98},"SessionID ",[77,2203,2204],{"class":1473},"string",[77,2206,2207],{"class":102}," `json:\"session_id\"`\n",[77,2209,2210,2212,2215,2217],{"class":79,"data-line":194,"line":194},[77,2211,1523],{"class":90},[77,2213,2214],{"class":98},"Pkg       ",[77,2216,2204],{"class":1473},[77,2218,2219],{"class":102}," `json:\"pkg\"`\n",[77,2221,2222,2224,2227,2229],{"class":79,"data-line":209,"line":209},[77,2223,1523],{"class":90},[77,2225,2226],{"class":98},"Cou       ",[77,2228,2204],{"class":1473},[77,2230,2231],{"class":102}," `json:\"cou\"`\n",[77,2233,2234,2236,2239,2241],{"class":79,"data-line":224,"line":224},[77,2235,1523],{"class":90},[77,2237,2238],{"class":98},"DID       ",[77,2240,2204],{"class":1473},[77,2242,2243],{"class":102}," `json:\"did\"`\n",[77,2245,2246],{"class":79,"data-line":237,"line":237},[77,2247,109],{"class":83},[10,2249,2250],{},"创建数据并将它序列化为 JSON：",[45,2252,2254],{"className":1465,"code":2253,"language":1237,"meta":51,"style":51},"payload := Payload{\n\tUID:       1008676840,\n\tSessionID: uuid.NewString(),\n\tPkg:       \"com.codepzj.app.android\",\n\tCou:       \"CN\",\n\tDID:       uuid.NewString(),\n}\n\npayloadBytes, err := json.Marshal(payload)\nif err != nil {\n\tlog.Fatalf(\"marshal payload failed: %v\", err)\n}\n",[17,2255,2256,2267,2278,2294,2305,2316,2331,2335,2339,2359,2373,2399],{"__ignoreMap":51},[77,2257,2258,2261,2263,2265],{"class":79,"data-line":80,"line":80},[77,2259,2260],{"class":98},"payload ",[77,2262,1529],{"class":1473},[77,2264,2174],{"class":1236},[77,2266,84],{"class":83},[77,2268,2269,2271,2274,2276],{"class":79,"data-line":87,"line":87},[77,2270,1523],{"class":90},[77,2272,2273],{"class":98},"UID:       ",[77,2275,174],{"class":94},[77,2277,177],{"class":98},[77,2279,2280,2282,2285,2288,2290,2292],{"class":79,"data-line":106,"line":106},[77,2281,1523],{"class":90},[77,2283,2284],{"class":98},"SessionID: uuid.",[77,2286,2287],{"class":1236},"NewString",[77,2289,1480],{"class":971},[77,2291,1490],{"class":971},[77,2293,177],{"class":98},[77,2295,2296,2298,2301,2303],{"class":79,"data-line":194,"line":194},[77,2297,1523],{"class":90},[77,2299,2300],{"class":98},"Pkg:       ",[77,2302,204],{"class":102},[77,2304,177],{"class":98},[77,2306,2307,2309,2312,2314],{"class":79,"data-line":209,"line":209},[77,2308,1523],{"class":90},[77,2310,2311],{"class":98},"Cou:       ",[77,2313,219],{"class":102},[77,2315,177],{"class":98},[77,2317,2318,2320,2323,2325,2327,2329],{"class":79,"data-line":224,"line":224},[77,2319,1523],{"class":90},[77,2321,2322],{"class":98},"DID:       uuid.",[77,2324,2287],{"class":1236},[77,2326,1480],{"class":971},[77,2328,1490],{"class":971},[77,2330,177],{"class":98},[77,2332,2333],{"class":79,"data-line":237,"line":237},[77,2334,109],{"class":83},[77,2336,2337],{"class":79,"data-line":877,"line":877},[77,2338,1590],{"emptyLinePlaceholder":1589},[77,2340,2341,2344,2346,2349,2352,2354,2357],{"class":79,"data-line":1066,"line":1066},[77,2342,2343],{"class":98},"payloadBytes, err ",[77,2345,1529],{"class":1473},[77,2347,2348],{"class":98}," json.",[77,2350,2351],{"class":1236},"Marshal",[77,2353,1480],{"class":83},[77,2355,2356],{"class":98},"payload",[77,2358,1542],{"class":83},[77,2360,2361,2363,2365,2367,2369,2371],{"class":79,"data-line":1081,"line":1081},[77,2362,1549],{"class":1473},[77,2364,1552],{"class":98},[77,2366,1555],{"class":1473},[77,2368,1558],{"class":94},[77,2370,1493],{"class":98},[77,2372,84],{"class":83},[77,2374,2375,2377,2380,2383,2385,2388,2391,2394,2397],{"class":79,"data-line":1092,"line":1092},[77,2376,1523],{"class":90},[77,2378,2379],{"class":98},"log.",[77,2381,2382],{"class":1236},"Fatalf",[77,2384,1480],{"class":971},[77,2386,2387],{"class":102},"\"marshal payload failed: ",[77,2389,2390],{"class":94},"%v",[77,2392,2393],{"class":102},"\"",[77,2395,2396],{"class":98},", err",[77,2398,1542],{"class":971},[77,2400,2401],{"class":79,"data-line":1102,"line":1102},[77,2402,109],{"class":83},[10,2404,2405,2408],{},[17,2406,2407],{"code":2407},"jws.Sign"," 接收字节切片，它并不要求 Payload 一定是 JSON。这里先进行 JSON 序列化，是为了便于在不同服务和语言之间传递结构化数据。",[37,2410,2412],{"id":2411},"使用私钥和-rs256-签名","使用私钥和 RS256 签名",[10,2414,2415,2416,1846],{},"先读取私钥，再调用 ",[17,2417,2407],{"code":2407},[45,2419,2421],{"className":1465,"code":2420,"language":1237,"meta":51,"style":51},"privateKey, err := readRSAPrivateKeyFromPath(\".\u002Frsa\u002Fprivate.pem\")\nif err != nil {\n\tlog.Fatalf(\"parse RSA private key failed: %v\", err)\n}\n\nsigned, err := jws.Sign(\n\tpayloadBytes,\n\tjws.WithKey(jwa.RS256(), privateKey),\n)\nif err != nil {\n\tlog.Fatalf(\"sign payload failed: %v\", err)\n}\n\nfmt.Printf(\"signed: %s\\n\", signed)\n",[17,2422,2423,2439,2453,2474,2478,2482,2498,2505,2533,2537,2551,2572,2576,2580],{"__ignoreMap":51},[77,2424,2425,2428,2430,2432,2434,2437],{"class":79,"data-line":80,"line":80},[77,2426,2427],{"class":98},"privateKey, err ",[77,2429,1529],{"class":1473},[77,2431,1477],{"class":1236},[77,2433,1480],{"class":83},[77,2435,2436],{"class":102},"\".\u002Frsa\u002Fprivate.pem\"",[77,2438,1542],{"class":83},[77,2440,2441,2443,2445,2447,2449,2451],{"class":79,"data-line":87,"line":87},[77,2442,1549],{"class":1473},[77,2444,1552],{"class":98},[77,2446,1555],{"class":1473},[77,2448,1558],{"class":94},[77,2450,1493],{"class":98},[77,2452,84],{"class":83},[77,2454,2455,2457,2459,2461,2463,2466,2468,2470,2472],{"class":79,"data-line":106,"line":106},[77,2456,1523],{"class":90},[77,2458,2379],{"class":98},[77,2460,2382],{"class":1236},[77,2462,1480],{"class":971},[77,2464,2465],{"class":102},"\"parse RSA private key failed: ",[77,2467,2390],{"class":94},[77,2469,2393],{"class":102},[77,2471,2396],{"class":98},[77,2473,1542],{"class":971},[77,2475,2476],{"class":79,"data-line":194,"line":194},[77,2477,109],{"class":83},[77,2479,2480],{"class":79,"data-line":209,"line":209},[77,2481,1590],{"emptyLinePlaceholder":1589},[77,2483,2484,2487,2489,2492,2495],{"class":79,"data-line":224,"line":224},[77,2485,2486],{"class":98},"signed, err ",[77,2488,1529],{"class":1473},[77,2490,2491],{"class":98}," jws.",[77,2493,2494],{"class":1236},"Sign",[77,2496,2497],{"class":83},"(\n",[77,2499,2500,2502],{"class":79,"data-line":237,"line":237},[77,2501,1523],{"class":90},[77,2503,2504],{"class":98},"payloadBytes,\n",[77,2506,2507,2509,2512,2515,2517,2520,2522,2524,2526,2529,2531],{"class":79,"data-line":877,"line":877},[77,2508,1523],{"class":90},[77,2510,2511],{"class":98},"jws.",[77,2513,2514],{"class":1236},"WithKey",[77,2516,1480],{"class":971},[77,2518,2519],{"class":98},"jwa.",[77,2521,925],{"class":1236},[77,2523,1480],{"class":983},[77,2525,1490],{"class":983},[77,2527,2528],{"class":98},", privateKey",[77,2530,1490],{"class":971},[77,2532,177],{"class":98},[77,2534,2535],{"class":79,"data-line":1066,"line":1066},[77,2536,1542],{"class":83},[77,2538,2539,2541,2543,2545,2547,2549],{"class":79,"data-line":1081,"line":1081},[77,2540,1549],{"class":1473},[77,2542,1552],{"class":98},[77,2544,1555],{"class":1473},[77,2546,1558],{"class":94},[77,2548,1493],{"class":98},[77,2550,84],{"class":83},[77,2552,2553,2555,2557,2559,2561,2564,2566,2568,2570],{"class":79,"data-line":1092,"line":1092},[77,2554,1523],{"class":90},[77,2556,2379],{"class":98},[77,2558,2382],{"class":1236},[77,2560,1480],{"class":971},[77,2562,2563],{"class":102},"\"sign payload failed: ",[77,2565,2390],{"class":94},[77,2567,2393],{"class":102},[77,2569,2396],{"class":98},[77,2571,1542],{"class":971},[77,2573,2574],{"class":79,"data-line":1102,"line":1102},[77,2575,109],{"class":83},[77,2577,2578],{"class":79,"data-line":1690,"line":1690},[77,2579,1590],{"emptyLinePlaceholder":1589},[77,2581,2582,2585,2588,2590,2593,2596,2598,2601],{"class":79,"data-line":1707,"line":1707},[77,2583,2584],{"class":98},"fmt.",[77,2586,2587],{"class":1236},"Printf",[77,2589,1480],{"class":83},[77,2591,2592],{"class":102},"\"signed: ",[77,2594,2595],{"class":94},"%s\\n",[77,2597,2393],{"class":102},[77,2599,2600],{"class":98},", signed",[77,2602,1542],{"class":83},[10,2604,2605,2608,2609,2612],{},[17,2606,2607],{"code":2607},"jwa.RS256()"," 明确指定签名算法为 RS256，",[17,2610,2611],{"code":2611},"privateKey"," 则用于产生签名。生成的结果类似：",[45,2614,2617],{"className":2615,"code":2616,"language":50,"meta":51},[48],"eyJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjEwMDg2NzY4NDAs...\u003C省略>.X21n...\u003C省略>\n",[17,2618,2616],{"__ignoreMap":51},[10,2620,2621],{},"第一部分解码后是受保护头：",[45,2623,2624],{"className":71,"code":72,"language":73,"meta":51,"style":51},[17,2625,2626,2630,2640],{"__ignoreMap":51},[77,2627,2628],{"class":79,"data-line":80,"line":80},[77,2629,84],{"class":83},[77,2631,2632,2634,2636,2638],{"class":79,"data-line":87,"line":87},[77,2633,91],{"class":90},[77,2635,95],{"class":94},[77,2637,99],{"class":98},[77,2639,103],{"class":102},[77,2641,2642],{"class":79,"data-line":106,"line":106},[77,2643,109],{"class":83},[10,2645,2646],{},"第二部分是原始 JSON 载荷的 Base64URL 编码，第三部分才是真正的签名值。Payload 或受保护头只要有一个字节发生改变，原签名就无法通过验证。",[37,2648,2650],{"id":2649},"使用公钥验签并还原-payload","使用公钥验签并还原 Payload",[10,2652,2653],{},"验证方读取公钥，并通过同一个 RS256 算法验证签名：",[45,2655,2657],{"className":1465,"code":2656,"language":1237,"meta":51,"style":51},"publicKey, err := readRSAPublicKeyFromPath(\".\u002Frsa\u002Fpublic.pem\")\nif err != nil {\n\tlog.Fatalf(\"parse RSA public key failed: %v\", err)\n}\n\nbody, err := jws.Verify(\n\tsigned,\n\tjws.WithKey(jwa.RS256(), publicKey),\n)\nif err != nil {\n\tlog.Fatalf(\"verify JWS failed: %v\", err)\n}\n",[17,2658,2659,2675,2689,2710,2714,2718,2732,2739,2764,2768,2782,2803],{"__ignoreMap":51},[77,2660,2661,2664,2666,2668,2670,2673],{"class":79,"data-line":80,"line":80},[77,2662,2663],{"class":98},"publicKey, err ",[77,2665,1529],{"class":1473},[77,2667,1858],{"class":1236},[77,2669,1480],{"class":83},[77,2671,2672],{"class":102},"\".\u002Frsa\u002Fpublic.pem\"",[77,2674,1542],{"class":83},[77,2676,2677,2679,2681,2683,2685,2687],{"class":79,"data-line":87,"line":87},[77,2678,1549],{"class":1473},[77,2680,1552],{"class":98},[77,2682,1555],{"class":1473},[77,2684,1558],{"class":94},[77,2686,1493],{"class":98},[77,2688,84],{"class":83},[77,2690,2691,2693,2695,2697,2699,2702,2704,2706,2708],{"class":79,"data-line":106,"line":106},[77,2692,1523],{"class":90},[77,2694,2379],{"class":98},[77,2696,2382],{"class":1236},[77,2698,1480],{"class":971},[77,2700,2701],{"class":102},"\"parse RSA public key failed: ",[77,2703,2390],{"class":94},[77,2705,2393],{"class":102},[77,2707,2396],{"class":98},[77,2709,1542],{"class":971},[77,2711,2712],{"class":79,"data-line":194,"line":194},[77,2713,109],{"class":83},[77,2715,2716],{"class":79,"data-line":209,"line":209},[77,2717,1590],{"emptyLinePlaceholder":1589},[77,2719,2720,2723,2725,2727,2730],{"class":79,"data-line":224,"line":224},[77,2721,2722],{"class":98},"body, err ",[77,2724,1529],{"class":1473},[77,2726,2491],{"class":98},[77,2728,2729],{"class":1236},"Verify",[77,2731,2497],{"class":83},[77,2733,2734,2736],{"class":79,"data-line":237,"line":237},[77,2735,1523],{"class":90},[77,2737,2738],{"class":98},"signed,\n",[77,2740,2741,2743,2745,2747,2749,2751,2753,2755,2757,2760,2762],{"class":79,"data-line":877,"line":877},[77,2742,1523],{"class":90},[77,2744,2511],{"class":98},[77,2746,2514],{"class":1236},[77,2748,1480],{"class":971},[77,2750,2519],{"class":98},[77,2752,925],{"class":1236},[77,2754,1480],{"class":983},[77,2756,1490],{"class":983},[77,2758,2759],{"class":98},", publicKey",[77,2761,1490],{"class":971},[77,2763,177],{"class":98},[77,2765,2766],{"class":79,"data-line":1066,"line":1066},[77,2767,1542],{"class":83},[77,2769,2770,2772,2774,2776,2778,2780],{"class":79,"data-line":1081,"line":1081},[77,2771,1549],{"class":1473},[77,2773,1552],{"class":98},[77,2775,1555],{"class":1473},[77,2777,1558],{"class":94},[77,2779,1493],{"class":98},[77,2781,84],{"class":83},[77,2783,2784,2786,2788,2790,2792,2795,2797,2799,2801],{"class":79,"data-line":1092,"line":1092},[77,2785,1523],{"class":90},[77,2787,2379],{"class":98},[77,2789,2382],{"class":1236},[77,2791,1480],{"class":971},[77,2793,2794],{"class":102},"\"verify JWS failed: ",[77,2796,2390],{"class":94},[77,2798,2393],{"class":102},[77,2800,2396],{"class":98},[77,2802,1542],{"class":971},[77,2804,2805],{"class":79,"data-line":1102,"line":1102},[77,2806,109],{"class":83},[10,2808,2809,2812],{},[17,2810,2811],{"code":2811},"jws.Verify"," 只有在签名合法时才返回经过验证的 Payload。不要在验签之前信任或使用载荷中的业务字段。",[10,2814,2815],{},"最后将验证后的 JSON 反序列化回结构体：",[45,2817,2819],{"className":1465,"code":2818,"language":1237,"meta":51,"style":51},"var verifiedPayload Payload\nif err := json.Unmarshal(body, &verifiedPayload); err != nil {\n\tlog.Fatalf(\"unmarshal payload failed: %v\", err)\n}\n\nfmt.Printf(\"verified payload: %+v\\n\", verifiedPayload)\n",[17,2820,2821,2832,2869,2890,2894,2898],{"__ignoreMap":51},[77,2822,2823,2826,2829],{"class":79,"data-line":80,"line":80},[77,2824,2825],{"class":1473},"var",[77,2827,2828],{"class":98}," verifiedPayload ",[77,2830,2831],{"class":1236},"Payload\n",[77,2833,2834,2836,2838,2840,2842,2845,2847,2850,2853,2856,2858,2861,2863,2865,2867],{"class":79,"data-line":87,"line":87},[77,2835,1549],{"class":1473},[77,2837,1552],{"class":98},[77,2839,1529],{"class":1473},[77,2841,2348],{"class":98},[77,2843,2844],{"class":1236},"Unmarshal",[77,2846,1480],{"class":83},[77,2848,2849],{"class":98},"body, ",[77,2851,2852],{"class":1473},"&",[77,2854,2855],{"class":98},"verifiedPayload",[77,2857,1490],{"class":83},[77,2859,2860],{"class":98},"; err ",[77,2862,1555],{"class":1473},[77,2864,1558],{"class":94},[77,2866,1493],{"class":98},[77,2868,84],{"class":83},[77,2870,2871,2873,2875,2877,2879,2882,2884,2886,2888],{"class":79,"data-line":106,"line":106},[77,2872,1523],{"class":90},[77,2874,2379],{"class":98},[77,2876,2382],{"class":1236},[77,2878,1480],{"class":971},[77,2880,2881],{"class":102},"\"unmarshal payload failed: ",[77,2883,2390],{"class":94},[77,2885,2393],{"class":102},[77,2887,2396],{"class":98},[77,2889,1542],{"class":971},[77,2891,2892],{"class":79,"data-line":194,"line":194},[77,2893,109],{"class":83},[77,2895,2896],{"class":79,"data-line":209,"line":209},[77,2897,1590],{"emptyLinePlaceholder":1589},[77,2899,2900,2902,2904,2906,2909,2912,2914,2917],{"class":79,"data-line":224,"line":224},[77,2901,2584],{"class":98},[77,2903,2587],{"class":1236},[77,2905,1480],{"class":83},[77,2907,2908],{"class":102},"\"verified payload: ",[77,2910,2911],{"class":94},"%+v\\n",[77,2913,2393],{"class":102},[77,2915,2916],{"class":98},", verifiedPayload",[77,2918,1542],{"class":83},[10,2920,2921],{},"完整的数据流如下：",[294,2923,2924,2927,2934,2937,2945,2948,2953,2956],{},[10,2925,2926],{},"{Payload 结构体}",[10,2928,2929,2930,2933],{},"调用 ",[17,2931,2932],{"code":2932},"json.Marshal"," 得到 JSON 字节。",[10,2935,2936],{},"{生成 JWS}",[10,2938,2929,2939,2941,2942,680],{},[17,2940,2407],{"code":2407},"，使用 RS256 和 RSA 私钥生成 ",[17,2943,2944],{"code":2944},"Header.Payload.Signature",[10,2946,2947],{},"{验证 JWS}",[10,2949,2929,2950,2952],{},[17,2951,2811],{"code":2811},"，使用 RS256 和 RSA 公钥验证签名并取出 JSON 字节。",[10,2954,2955],{},"{还原 Payload}",[10,2957,2929,2958,2961],{},[17,2959,2960],{"code":2960},"json.Unmarshal"," 将验证后的 JSON 字节还原为结构体。",[37,2963,2964],{"id":2964},"运行示例",[10,2966,2967,2968,710,2971,2974,2975,2978],{},"由于代码使用了 ",[17,2969,2970],{"code":2970},".\u002Frsa\u002Fprivate.pem",[17,2972,2973],{"code":2973},".\u002Frsa\u002Fpublic.pem"," 相对路径，需要在 ",[17,2976,2977],{"code":2977},"jws"," 目录中运行：",[45,2980,2982],{"className":1227,"code":2981,"language":1229,"meta":51,"style":51},"cd jws\ngo run .\n",[17,2983,2984,2992],{"__ignoreMap":51},[77,2985,2986,2989],{"class":79,"data-line":80,"line":80},[77,2987,2988],{"class":94},"cd",[77,2990,2991],{"class":102}," jws\n",[77,2993,2994,2996,2999],{"class":79,"data-line":87,"line":87},[77,2995,1237],{"class":1236},[77,2997,2998],{"class":102}," run",[77,3000,3001],{"class":102}," .\n",[10,3003,3004],{},"输出类似：",[45,3006,3009],{"className":3007,"code":3008,"language":50,"meta":51},[48],"signed: eyJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjEwMDg2NzY4NDAs...\u003C省略>\nverified payload: {UID:1008676840 SessionID:27a5d73b-11be-4d97-a728-2d08e9a25771 Pkg:com.codepzj.app.android Cou:CN DID:1102bc81-6f25-4d08-8f8d-46bba8c62d62}\n",[17,3010,3008],{"__ignoreMap":51},[10,3012,3013,3014,3017,3018,3021,3022,3024],{},"如果在 ",[17,3015,3016],{"code":3016},"goplayground"," 根目录执行 ",[17,3019,3020],{"code":3020},"go run .\u002Fjws","，程序会从当前工作目录查找 ",[17,3023,2970],{"code":2970},"，因此可能出现下面的错误：",[45,3026,3029],{"className":3027,"code":3028,"language":50,"meta":51},[48],"open .\u002Frsa\u002Fprivate.pem: no such file or directory\n",[17,3030,3028],{"__ignoreMap":51},[10,3032,3033,3034,3037],{},"实际项目中可以通过配置项传入绝对路径，或使用 ",[17,3035,3036],{"code":3036},"go:embed"," 嵌入公钥。私钥一般不建议嵌入二进制文件。",[37,3039,3040],{"id":3040},"验证篡改后的数据",[10,3042,3043,3044,3047],{},"JWS 的价值在于能够发现数据是否被修改。可以在签名完成后手动改变 ",[17,3045,3046],{"code":3046},"signed"," 中 Payload 部分的任意字符，再进行验证：",[45,3049,3051],{"className":1465,"code":3050,"language":1237,"meta":51,"style":51},"body, err := jws.Verify(tampered, jws.WithKey(jwa.RS256(), publicKey))\nif err != nil {\n\tlog.Printf(\"signature verification failed: %v\", err)\n}\n",[17,3052,3053,3086,3100,3121],{"__ignoreMap":51},[77,3054,3055,3057,3059,3061,3063,3065,3068,3070,3072,3074,3076,3078,3080,3082,3084],{"class":79,"data-line":80,"line":80},[77,3056,2722],{"class":98},[77,3058,1529],{"class":1473},[77,3060,2491],{"class":98},[77,3062,2729],{"class":1236},[77,3064,1480],{"class":83},[77,3066,3067],{"class":98},"tampered, jws.",[77,3069,2514],{"class":1236},[77,3071,1480],{"class":971},[77,3073,2519],{"class":98},[77,3075,925],{"class":1236},[77,3077,1480],{"class":983},[77,3079,1490],{"class":983},[77,3081,2759],{"class":98},[77,3083,1490],{"class":971},[77,3085,1542],{"class":83},[77,3087,3088,3090,3092,3094,3096,3098],{"class":79,"data-line":87,"line":87},[77,3089,1549],{"class":1473},[77,3091,1552],{"class":98},[77,3093,1555],{"class":1473},[77,3095,1558],{"class":94},[77,3097,1493],{"class":98},[77,3099,84],{"class":83},[77,3101,3102,3104,3106,3108,3110,3113,3115,3117,3119],{"class":79,"data-line":106,"line":106},[77,3103,1523],{"class":90},[77,3105,2379],{"class":98},[77,3107,2587],{"class":1236},[77,3109,1480],{"class":971},[77,3111,3112],{"class":102},"\"signature verification failed: ",[77,3114,2390],{"class":94},[77,3116,2393],{"class":102},[77,3118,2396],{"class":98},[77,3120,1542],{"class":971},[77,3122,3123],{"class":79,"data-line":194,"line":194},[77,3124,109],{"class":83},[10,3126,3127],{},"即使修改后的 Base64URL 仍然能够正常解码，它也无法通过原公钥的签名验证，因为签名所覆盖的内容已经发生变化。",[37,3129,3130],{"id":3130},"生产环境需要继续补充什么",[10,3132,3133],{},"这个示例打通了签名和验签的最小链路，但生产环境通常还要补充以下能力：",[328,3135,3136],{},[331,3137,3138,3144,3157,3163,3171,3183,3189],{},[334,3139,3140,3143],{},[133,3141,3142],{},"时间限制","：载荷中加入签发时间和过期时间，并在验签后主动校验，避免签名永久有效。",[334,3145,3146,3149,3150,618,3153,3156],{},[133,3147,3148],{},"签发方与接收方限制","：校验 ",[17,3151,3152],{"code":3152},"issuer",[17,3154,3155],{"code":3155},"audience"," 等业务字段，避免合法签名被用到错误的系统。",[334,3158,3159,3162],{},[133,3160,3161],{},"防重放","：加入唯一请求 ID、时间戳或 nonce，并在服务端记录短期使用状态。",[334,3164,3165,3168,3169,680],{},[133,3166,3167],{},"算法白名单","：验签时由服务端固定允许的算法，不能盲目信任 Header 中的 ",[17,3170,123],{"code":123},[334,3172,3173,3176,3177,3179,3180,3182],{},[133,3174,3175],{},"密钥轮换","：通过 ",[17,3178,127],{"code":127}," 标识密钥版本，验证方根据 ",[17,3181,127],{"code":127}," 选择对应公钥。",[334,3184,3185,3188],{},[133,3186,3187],{},"私钥保护","：限制文件权限，避免写入日志，并使用 KMS、HSM 或 Secret 管理系统保存私钥。",[334,3190,3191,3194],{},[133,3192,3193],{},"错误处理","：对外返回统一的验签失败信息，对内记录可追踪但不泄露敏感数据的日志。",[10,3196,3197,3198,3201,3202,3205],{},"如果载荷本身就是认证 Claims，并且需要标准的过期时间、签发方和接收方校验，可以在同一个 ",[17,3199,3200],{"code":3200},"jwx"," 项目中继续使用 ",[17,3203,3204],{"code":3204},"jwt"," 包建模和验证 JWT，而不是手动重复实现全部规则。",[37,3207,3208],{"id":3208},"总结",[10,3210,3211,3212,3214,3215,3217],{},"使用 Go 实现 RS256 JWS 的核心只有两步：签名方调用 ",[17,3213,2407],{"code":2407}," 并传入 RSA 私钥，验证方调用 ",[17,3216,2811],{"code":2811}," 并传入 RSA 公钥。真正容易出错的地方，反而是密钥格式、相对路径和安全边界。",[10,3219,3220],{},"需要记住：",[331,3222,3223,3226,3229,3235,3241],{},[334,3224,3225],{},"JWS 用于签名，不负责加密 Payload；",[334,3227,3228],{},"RS256 使用私钥签名、公钥验签；",[334,3230,3231,3232,3234],{},"PEM 文件头必须与 ",[17,3233,1437],{"code":1437}," 的解析函数匹配；",[334,3236,3237,3238,3240],{},"只有 ",[17,3239,2811],{"code":2811}," 成功后，才能信任返回的 Payload；",[334,3242,3243],{},"生产环境还需要处理过期时间、防重放、算法限制和密钥轮换。",[10,3245,3246],{},"理解这些基础后，就可以把同样的签名流程应用到 API 鉴权、服务间消息、Webhook 回调和配置下发等场景中。",[37,3248,3249],{"id":3249},"参考资料",[331,3251,3252,3260,3266],{},[334,3253,3254],{},[3255,3256,3257],"a",{"href":3257,"rel":3258},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc7515",[3259],"nofollow",[334,3261,3262],{},[3255,3263,3264],{"href":3264,"rel":3265},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc7518",[3259],[334,3267,3268],{},[3255,3269,3270],{"href":3270,"rel":3271},"https:\u002F\u002Fpkg.go.dev\u002Fgithub.com\u002Flestrrat-go\u002Fjwx\u002Fv4\u002Fjws",[3259],[3273,3274,3275],"style",{},"html pre.shiki code .sSDpw, html code.shiki .sSDpw{--shiki-default:#005cc5;--shiki-dark:#79b8ff}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJX2D, html code.shiki .sJX2D{--shiki-default:#e36209;--shiki-dark:#ffab70}html pre.shiki code .sqkie, html code.shiki .sqkie{--shiki-default:#5a32a3;--shiki-dark:#b392f0}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":51,"searchDepth":194,"depth":194,"links":3277},[3278,3283,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302],{"id":39,"depth":87,"text":40,"children":3279},[3280,3281,3282],{"id":64,"depth":106,"text":65},{"id":145,"depth":106,"text":146},{"id":245,"depth":106,"text":246},{"id":322,"depth":87,"text":323,"children":3284},[3285,3286,3287,3288,3289],{"id":460,"depth":106,"text":461},{"id":626,"depth":106,"text":627},{"id":652,"depth":106,"text":653},{"id":779,"depth":106,"text":780},{"id":1122,"depth":106,"text":1123},{"id":1186,"depth":87,"text":1187},{"id":1217,"depth":87,"text":1217},{"id":1264,"depth":87,"text":1265},{"id":1430,"depth":87,"text":1431},{"id":1835,"depth":87,"text":1836},{"id":2157,"depth":87,"text":2158},{"id":2411,"depth":87,"text":2412},{"id":2649,"depth":87,"text":2650},{"id":2964,"depth":87,"text":2964},{"id":3040,"depth":87,"text":3040},{"id":3130,"depth":87,"text":3130},{"id":3208,"depth":87,"text":3208},{"id":3249,"depth":87,"text":3249},[3304],"Go语言","2026-08-13","通过 lestrrat-go\u002Fjwx v4 完成一个 JWS 示例：使用 OpenSSL 生成 RSA 密钥，解析 PEM 文件，以 RS256 对 JSON 载荷签名，并使用公钥验证签名。",false,"md",null,{"slots":3311},{},"\u002F2026\u002F08\u002Fgo-jws",{"text":3314,"minutes":3315,"time":3316,"words":3317},"23 min read",22.785,1367100,4557,{"title":5,"description":3306},{"loc":3312},"posts\u002F2026\u002F08\u002Fgo-jws",[3322,408,925,893,3323],"Go","JOSE","tech","tQ5w3R7jz5ev5fc4WVHDAKeH-cOjvhEBdmuNdrwBvE4",[3327,3309],{"title":3328,"path":3329,"stem":3330,"date":3331,"type":3324,"children":-1},"macOS 使用华为手机调试 Flutter：ADB 设备为空排查","\u002F2026\u002F08\u002Fflutter-huawei-usb-device-debugging","posts\u002F2026\u002F08\u002Fflutter-huawei-usb-device-debugging","2026-08-04",1786555052636]