引言
和风天气开发服务 使用JWT(JSON Web Token)以及API KEY的方式进行身份认证。和风官方推荐使用JWT方式。虽然很多语言都有完备的JWT第三方库,但是因为最近在学习GO语言,刚好借这个机会熟悉go语言。整个代码的实现半手工半deepseek,所以代码风格和细节会很糙,不过勉强能够成功访问并返回当前天气,至于优化,那就后面随着技术提升慢慢来吧。
账号注册和项目配置
账号注册和配置就看官方文档吧,以及计费规则,和风的计价是分梯度的,不同类型的天气数据有不同的收费标准,在写文章的时候,天气数据是有每个月5万次的免费访问额度,对我作为学习开发来说是足够了。
JWT数据
整个token可以看作由三部分组成,Header、Payload、Signature,其中,Header包含alg和kid,Payload包含sub、iat、exp,Signature则是Header和Payload的签名。
/*type JWT struct {
Header Header `json:"header"`
Payload Payload `json:"payload"`
Signature string `json:"signature"`
}
*/
type Header struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
type Payload struct {
Sub string `json:"sub"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
}
Header
alg:签名使用的算法,在这里我们指定“EdDSA”kid:凭据id。账号注册完毕后,新建项目,在项目内新建凭据,新建完毕后会有一串字符串
Payload
sub:项目id,新建项目后就会有iat:签发时间,UNIX时间戳格式,为了防止时间误差,官方会建议大家使用当前时间之前30s的时间戳exp:过期时间,UNIX时间戳格式,代表token的过期时间,较长的过期时间可以减轻负载,但是较短的时间可以提高安全性。
Signature
Signature 是先将Header和Payload分别进行Base64URL编码,并用英文句号拼接在一起,然后使用私钥进行Ed25519算法的签名,之后对签名结果同样进行Base64URL编码。
公钥和私钥
和风天气使用Ed25519算法进行签名,Ed25519是使用Curve25519椭圆曲线和SHA-512的EdDSA(Edwards-curve Digital Signature Algorithm)的一种实现。你需要提前生成Ed25519的私钥和公钥,其中私钥用于签名且由你自己保管,公钥用于和风对签名真伪进行验证。这意味着除了你之外,任何人都无法伪造签名。
生成Ed25519密钥 这里介绍一种使用OpenSSL创建Ed25519密钥的方法。
提示:也可以通过在线工具、熟悉的开发语言或第三方库生成Ed25519私钥和公钥。 打开终端,粘贴下列文本生成公钥和私钥:
openssl genpkey -algorithm ED25519 -out ed25519-private.pem \
&& openssl pkey -pubout -in ed25519-private.pem > ed25519-public.pem
这将在当前目录创建两个文件:
ed25519-private.pem,私钥,用于JWT认证的签名。你应该妥善安全的保管私钥。
ed25519-public.pem,公钥,用于签名的验证,需要上传到和风天气控制台 上传公钥 当你完成密钥对的生成后,你需要将其中的公钥添加到和风天气控制台,用于JWT身份验证。
前往控制台-项目管理在项目列表中点击你需要添加凭据的项目点击凭据区域右侧的“添加凭据”按钮输入凭据名称选择身份认证方式JSON Web Token使用任意文本编辑器打开公钥文件(比如刚才创建的ed25519-public.pem),复制其中的全部内容, 在公钥文本框中粘贴公钥内容,这些内容看起来像是:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAARbeZ5AhklFG4gg1Gx5g5bWxMMdsUd6b2MC4wV0/M9Q=
-----END PUBLIC KEY-----
你将在最后看到创建凭据成功的页面,并且显示了这个凭据的创建日期、ID和SHA256值。出于安全考虑,和风控制台不会再次显示这个公钥。但你可以使用公钥的SHA256值与本地SHA256进行对比,以便确认使用的是正确的公钥。
最终的token
将编码后的Header\Payload\Signature用英文句号拼接在一起,形成最终的token,最终发送的JWT请求,需要将完整的Token作为参数添加到Authorization:bearer 请求标头,例如
curl --compressed \
-H 'Authorization: Bearer token' \
'https://api.qweather.com/v7/weather/now?location=101010100'
实际使用的时候,api.qweather.com需要替换为你自己的api,token需要替换为生成的token,location一样,根据官方的地址表查询。
Header+'.'+Payload+'.'+Signature
具体实现
1、新建go项目 新建一个文件夹,并进行项目初始化
go mod init projname
新建一个weather.go的文件,就可以开始进行编程了,我用的Visual Studio Code开发的,并安装了go插件,只要通过go get 安装过,VSC就会自动补全项目的import部分,后面不再单独说明import用到的库。
package main
import(
"fmt"
)
2、定义结构体和变量
type Header struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
type Payload struct {
Sub string `json:"sub"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
}
var SHeader = Header{}
var SPayload = Payload{}
var Token = ""
var PrivateKey = ""
var HeaderBase64URL = ""
var PayloadBase64URL = ""
3、变量获取 在同级文件夹下新建一个“.env”文件,把项目的配置信息放进去,kid和sub替换成实际项目信息
alg="EdDSA"
kid="C******97"
sub="27*********A"
变量获取函数,把Header和payload的信息填充进去
func GetEnvMessage() {
err := godotenv.Load()
if err != nil {
panic("Error loading .env file")
}
//PrivateKey = os.Getenv("key")
SHeader.Alg = os.Getenv("alg")
SHeader.Kid = os.Getenv("kid")
SPayload.Sub = os.Getenv("sub")
SPayload.Iat = time.Now().Add(time.Minute * -1).Unix()
SPayload.Exp = time.Now().Add(time.Minute * 5).Unix()
}
4、结构体转换成JSON并返回Base64URL编码 interface{}代表任意格式,这样Header和Payload都可以使用它转换成JSON字符串并进行Base64URL编码了。
func struct2Base64URL(s interface{}) string {
str, err := json.Marshal(s)
if err != nil {
log.Fatalf("JSON marshaling failed:%s", err)
}
b64 := base64.RawURLEncoding.EncodeToString([]byte(string(str)))
return b64
}
这里要使用带Raw的Encoding
base64.RawURLEncoding.EncodeToString([]byte(string(str)))
5、生成signature 先把Header和Payload进行base64url编码,然后进行拼接。 读取本地私钥,并用私钥对拼接的Header+payload进行加密以及Base64URL编码
func GenSignature() {
HeaderBase64URL = struct2Base64URL(SHeader)
PayloadBase64URL = struct2Base64URL(SPayload)
//读取私钥文件
privateKeyPEM, err := os.ReadFile("ed25519-private.pem")
if err != nil {
log.Fatalf("Error reading private key file: %v", err)
}
//解析私钥
block, _ := pem.Decode(privateKeyPEM)
if block == nil || block.Type != "PRIVATE KEY" {
log.Fatal("Failed to decode PEM block containing private key")
}
//PKCS#8解析
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
log.Fatalf("Error parsing private key: %v", err)
}
ed25519Key, ok := privateKey.(ed25519.PrivateKey)
if !ok {
log.Fatal("Not an ED25519 private key")
}
//获得私钥数据
PrivateKey = string(ed25519Key)
//数据加密
sig := ed25519.Sign(ed25519.PrivateKey(PrivateKey), []byte(HeaderBase64URL+"."+PayloadBase64URL))
//数据Base64编码
SignatureBase64URL =base64.URLEncoding.EncodeToString(sig)
}
6、生成token
Token = HeaderBase64URL + "." + PayloadBase64URL + "." + SignatureBase64URL
测试
其实生成了token就已经可以结束了。可以直接利用诸如curl的工具进行测试了,deepseek帮我写了一个测试代码,以及返回值的gzip解析以及打印,一起放这里吧,url记得换成自己的API-URl 。
client := &http.Client{
Transport: &http2.Transport{},
Timeout: 30 * time.Second,
} // 3. 发送请求
req, err := http.NewRequest("GET", "https://mh*****a6.re.qweatherapi.com/v7/weather/now?location=101010100", nil)
if err != nil {
log.Fatal(err)
}
req.Header = http.Header{
"Authorization": []string{"Bearer " + Token},
"Accept-Encoding": []string{"gzip"},
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 自动解压响应体
var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
if err != nil {
log.Fatal(err)
}
defer reader.Close()
default:
reader = resp.Body
}
// 读取解压后的数据
body, err := io.ReadAll(reader)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %d\n", resp.StatusCode)
fmt.Printf("Body:\n%s\n", body)
这个时候进行go run 就可以看到返回信息了,STATUS:200,成功。
Status: 200
Body:
{"code":"200","updateTime":"2025-05-10T15:36+08:00","fxLink":"https://www.qweather.com/en/weather/beijing-101010100.html","now":{"obsTime":"2025-05-10T15:30+08:00","temp":"25","feelsLike":"23","icon":"100","text":"晴","wind360":"270","windDir":"西风","windScale":"2","windSpeed":"7","humidity":"15","precip":"0.0","pressure":"1005","vis":"14","cloud":"10","dew":"4"},"refer":{"sources":["QWeather"],"license":["QWeather Developers License"]}}
主要是为了辅助自己理解JWT的过程,下一步可以尝试直接使用第三方库,减少代码量,以及重复造轮子。