英文:
Golang token validation error
问题
我需要验证一个 Google id_token,其中一步是检查令牌的签名。
首先,我从以下网址获取证书:https://www.googleapis.com/oauth2/v2/certs,并从证书中提取模数(n)和指数(e)部分,并生成一个公钥。然后,我拆分令牌(头部、有效载荷和摘要),然后将解码后的header.payload
与 Google 公钥和摘要一起发送到 rsa 函数rsa.VerifyPKCS1v15
。
我在验证过程中遇到了这个错误:crypto/rsa: verification error
以下是代码(我已经用// validation here fails
注释了验证失败的部分):
func ValidateIDToken(auth_token string) (err error){
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
log.Fatal(err)
return err
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
return err
}
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
k := goCertificate.(map[string]interface{})["keys"]
j := k.([]interface{})
x := j[1]
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
e64 := base64.StdEncoding
//build the google pub key
nStr := h.(string)
decN, err := base64.StdEncoding.DecodeString(nStr)
if err != nil {
log.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.StdEncoding.DecodeString(eStr)
if err != nil {
log.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
w := strings.SplitAfter(auth_token, ".")
for i, val := range w {
w[i] = strings.Trim(val, ".")
}
y := w[0:2]
//Join just the first two parts, the header and the payload without the signature
o := strings.Join(y, ".")
headerOauth := DecodeB64(nil,[]byte(w[0]),e64)
inblockOauth := DecodeB64(nil,[]byte(w[1]),e64)
toHash := string(headerOauth) + "}.&" + string(inblockOauth)
digestOauth := DecodeB64(nil, []byte(w[2]),e64)
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// validation here fails
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
log.Printf("Error verifying key %s",err.Error())
return err
}
return err
}
更新 1:
这是包含头部和有效载荷的 toHash 变量:
{"alg":"RS256","kid":"d91c503452d0f8849200a321ffbf7dea76f9371d"}.{"iss":"accounts.google.com","sub":"104869993929250743503","azp":"client_email_till_@.apps.googleusercontent.com","email":"test@test.hr","at_hash":"KAm1M0g-ssMkdjds7jkbVQ","email_verified":true,"aud":client_email_till_@.apps.googleusercontent.com","hd":"test.hr","iat":1412246551,"exp":1412250451}
更新 2:
感谢 @Florent Morselli 的回复,我再次尝试了一下,但仍然失败了。这次我只对第三部分(签名)进行了 B64 解码,但错误仍然存在。请问有人可以使用他们的 auth_token 进行测试吗?只需将 ID 令牌放入下面代码中的 auth_token 变量中,然后告诉我是否成功,谢谢。
package main
import(
"strings"
"encoding/binary"
"errors"
"fmt"
"log"
"encoding/base64"
"io/ioutil"
"crypto"
"crypto/sha256"
"crypto/rsa"
"bytes"
"encoding/json"
"net/http"
"math/big"
)
func main() {
auth_token := ""
w := strings.SplitAfter(auth_token, ".")
for i, val := range w {
w[i] = strings.Trim(val, ".")
}
headerOauth, err := base64.URLEncoding.DecodeString(w[0])
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
fmt.Println(err)
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
fmt.Println(err)
}
//extract kid from token header
var header interface{}
err = json.Unmarshal([]byte(string(headerOauth)+"}"), &header)
token_kid := header.(map[string]interface{})["kid"]
fmt.Println("By 1")
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
//k := goCertificate.(map[string]interface{})[token_kid.(string)]
k := goCertificate.(map[string]interface{})["keys"]
///*mod & exp part
j := k.([]interface{})
x := j[0]
if j[0].(map[string]interface{})["kid"] == token_kid {
x = j[0]
}else{
if j[1].(map[string]interface{})["kid"] == token_kid {
x = j[1]
}else{
errors.New("Token is not valid, kid from token and certificate don't match")
}
}
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
//build the google pub key
nStr := h.(string)
decN, err := base64.URLEncoding.DecodeString(nStr)
if err != nil {
fmt.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.URLEncoding.DecodeString(eStr)
if err != nil {
fmt.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
//inblockOauth := base64.URLEncoding.DecodeString(w[1])
toHash := w[0] + "." + w[1]
digestOauth, err := base64.URLEncoding.DecodeString(w[2])
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// verification here fails
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
fmt.Printf("Error verifying key %s",err.Error())
}
}
希望对你有所帮助!
英文:
I need to validate a google id_token and one step involves to check the token signature.
First I obtain the certificate from: https://www.googleapis.com/oauth2/v2/certs and extract the modulus (n) and exponent (e) part from the certificate and generate a public key, then I take apart the token (header, payload and digest), after then I send the decoded header.payload
together with the Google pKey + digest to the rsa function rsa.VerifyPKCS1v15
.
I am stuck with this verification error: crypto/rsa: verification error
Here's the code (I commented part of code which fails with // validation here fails
):
func ValidateIDToken(auth_token string) (err error){
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
log.Fatal(err)
return err
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
return err
}
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
k := goCertificate.(map[string]interface{})["keys"]
j := k.([]interface{})
x := j[1]
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
e64 := base64.StdEncoding
//build the google pub key
nStr := h.(string)
decN, err := base64.StdEncoding.DecodeString(nStr)
if err != nil {
log.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.StdEncoding.DecodeString(eStr)
if err != nil {
log.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
w := strings.SplitAfter(auth_token, ".")
for i, val := range w {
w[i] = strings.Trim(val, ".")
}
y := w[0:2]
//Join just the first two parts, the header and the payload without the signature
o := strings.Join(y, ".")
headerOauth := DecodeB64(nil,[]byte(w[0]),e64)
inblockOauth := DecodeB64(nil,[]byte(w[1]),e64)
toHash := string(headerOauth) + "}." + string(inblockOauth)
digestOauth := DecodeB64(nil, []byte(w[2]),e64)
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// validation here fails
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
log.Printf("Error verifying key %s",err.Error())
return err
}
return err
}
UPDATE 1:
Here is toHash var which contains header and payload:
{"alg":"RS256","kid":"d91c503452d0f8849200a321ffbf7dea76f9371d"}.{"iss":"accounts.google.com","sub":"104869993929250743503","azp":"client_email_till_@.apps.googleusercontent.com","email":"test@test.hr","at_hash":"KAm1M0g-ssMkdjds7jkbVQ","email_verified":true,"aud":client_email_till_@.apps.googleusercontent.com","hd":"test.hr","iat":1412246551,"exp":1412250451}
UPDATE 2:
Thanks for the reply @Florent Morselli, I tried it again and it failed, I B64decoded this time only the third part (signature) but the error was still there, could someone test it with their
auth_token, just put the ID token in auth_token variable below in the code, and let me know if it worked, thank You.
package main
import(
"strings"
"encoding/binary"
"errors"
"fmt"
"log"
"encoding/base64"
"io/ioutil"
"crypto"
"crypto/sha256"
"crypto/rsa"
"bytes"
"encoding/json"
"net/http"
"math/big"
)
func main() {
auth_token := ""
w := strings.SplitAfter(auth_token, ".")
for i, val := range w {
w[i] = strings.Trim(val, ".")
}
headerOauth, err := base64.URLEncoding.DecodeString(w[0])
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
fmt.Println(err)
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
fmt.Println(err)
}
//extract kid from token header
var header interface{}
err = json.Unmarshal([]byte(string(headerOauth)+"}"), &header)
token_kid := header.(map[string]interface{})["kid"]
fmt.Println("By 1")
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
//k := goCertificate.(map[string]interface{})[token_kid.(string)]
k := goCertificate.(map[string]interface{})["keys"]
///*mod & exp part
j := k.([]interface{})
x := j[0]
if j[0].(map[string]interface{})["kid"] == token_kid {
x = j[0]
}else{
if j[1].(map[string]interface{})["kid"] == token_kid {
x = j[1]
}else{
errors.New("Token is not valid, kid from token and certificate don't match")
}
}
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
//build the google pub key
nStr := h.(string)
decN, err := base64.URLEncoding.DecodeString(nStr)
if err != nil {
fmt.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.URLEncoding.DecodeString(eStr)
if err != nil {
fmt.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
//inblockOauth := base64.URLEncoding.DecodeString(w[1])
toHash := w[0] + "." + w[1]
digestOauth, err := base64.URLEncoding.DecodeString(w[2])
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// verification here fails
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
fmt.Printf("Error verifying key %s",err.Error())
}
}
答案1
得分: 5
根据聊天中的解释,问题在于如果头部和签名缺少"=",则Base64解码器无法解码它们。
你只需要使用以下代码添加它们:
if m := len(h_) % 4; m != 0 {
h_ += strings.Repeat("=", 4-m)
}
以下是完整的代码:
package main
import(
"strings"
"encoding/binary"
"errors"
"fmt"
"log"
"encoding/base64"
"io/ioutil"
"crypto"
"crypto/sha256"
"crypto/rsa"
"bytes"
"encoding/json"
"net/http"
"math/big"
)
func main() {
auth_token := ""
w := strings.Split(auth_token, ".")
h_, s_ := w[0], w[2]
if m := len(h_) % 4; m != 0 {
h_ += strings.Repeat("=", 4-m)
}
if m := len(s_) % 4; m != 0 {
s_ += strings.Repeat("=", 4-m)
}
headerOauth, err := base64.URLEncoding.DecodeString(h_)
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
fmt.Println(err)
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
fmt.Println(err)
}
//extract kid from token header
var header interface{}
err = json.Unmarshal([]byte(string(headerOauth)), &header)
token_kid := header.(map[string]interface{})["kid"]
fmt.Println("By 1")
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
//k := goCertificate.(map[string]interface{})[token_kid.(string)]
k := goCertificate.(map[string]interface{})["keys"]
///*mod & exp part
j := k.([]interface{})
x := j[0]
if j[0].(map[string]interface{})["kid"] == token_kid {
x = j[0]
}else{
if j[1].(map[string]interface{})["kid"] == token_kid {
x = j[1]
}else{
errors.New("Token is not valid, kid from token and certificate don't match")
}
}
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
//build the google pub key
nStr := h.(string)
decN, err := base64.URLEncoding.DecodeString(nStr)
if err != nil {
fmt.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.URLEncoding.DecodeString(eStr)
if err != nil {
fmt.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
//inblockOauth := base64.URLEncoding.DecodeString(w[1])
toHash := w[0] + "." + w[1]
digestOauth, err := base64.URLEncoding.DecodeString(s_)
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// verification of the signature
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
fmt.Printf("Error verifying key %s",err.Error())
}
fmt.Printf("OK!")
}
英文:
As explained on the chat, the problem is that the Base64 decoder is unable to decode the header and the signature if they are missing "=".
You just have to add them with the following code:
if m := len(h_) % 4; m != 0 {
h_ += strings.Repeat("=", 4-m)
}
Here is the complete code:
package main
import(
"strings"
"encoding/binary"
"errors"
"fmt"
"log"
"encoding/base64"
"io/ioutil"
"crypto"
"crypto/sha256"
"crypto/rsa"
"bytes"
"encoding/json"
"net/http"
"math/big"
)
func main() {
auth_token := ""
w := strings.Split(auth_token, ".")
h_, s_ := w[0], w[2]
if m := len(h_) % 4; m != 0 {
h_ += strings.Repeat("=", 4-m)
}
if m := len(s_) % 4; m != 0 {
s_ += strings.Repeat("=", 4-m)
}
headerOauth, err := base64.URLEncoding.DecodeString(h_)
res, err := http.Get("https://www.googleapis.com/oauth2/v2/certs")
if err != nil {
fmt.Println(err)
}
certs, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
fmt.Println(err)
}
//extract kid from token header
var header interface{}
err = json.Unmarshal([]byte(string(headerOauth)), &header)
token_kid := header.(map[string]interface{})["kid"]
fmt.Println("By 1")
//get modulus and exponent from the cert
var goCertificate interface{}
err = json.Unmarshal(certs, &goCertificate)
//k := goCertificate.(map[string]interface{})[token_kid.(string)]
k := goCertificate.(map[string]interface{})["keys"]
///*mod & exp part
j := k.([]interface{})
x := j[0]
if j[0].(map[string]interface{})["kid"] == token_kid {
x = j[0]
}else{
if j[1].(map[string]interface{})["kid"] == token_kid {
x = j[1]
}else{
errors.New("Token is not valid, kid from token and certificate don't match")
}
}
h := x.(map[string]interface{})["n"]
g := x.(map[string]interface{})["e"]
//build the google pub key
nStr := h.(string)
decN, err := base64.URLEncoding.DecodeString(nStr)
if err != nil {
fmt.Println(err)
return
}
n := big.NewInt(0)
n.SetBytes(decN)
eStr := g.(string)
decE, err := base64.URLEncoding.DecodeString(eStr)
if err != nil {
fmt.Println(err)
return
}
var eBytes []byte
if len(decE) < 8 {
eBytes = make([]byte, 8-len(decE), 8)
eBytes = append(eBytes, decE...)
} else {
eBytes = decE
}
eReader := bytes.NewReader(eBytes)
var e uint64
err = binary.Read(eReader, binary.BigEndian, &e)
if err != nil {
log.Println(err)
return
}
pKey := rsa.PublicKey{N: n, E: int(e)}
//inblockOauth := base64.URLEncoding.DecodeString(w[1])
toHash := w[0] + "." + w[1]
digestOauth, err := base64.URLEncoding.DecodeString(s_)
hasherOauth := sha256.New()
hasherOauth.Write([]byte(toHash))
// verification of the signature
err = rsa.VerifyPKCS1v15(&pKey,crypto.SHA256,hasherOauth.Sum(nil),digestOauth)
if err != nil {
fmt.Printf("Error verifying key %s",err.Error())
}
fmt.Printf("OK!")
}
答案2
得分: 2
不要使用StdEncoding,它不符合规范要求的URL安全性。
请使用URLEncoding代替。更多信息请参考https://gobyexample.com/base64-encoding。
Base64 Url Safe与Base64相同,但不包含'/'和'+'(被'_'和'-'替代),并且移除了尾部的'='。
英文:
Do not use the StdEncoding, it is not URL-Safe as required by the specification.
Use URLEncoding instead. See https://gobyexample.com/base64-encoding for more informations.
The Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
答案3
得分: 2
我将“decoded”头部和负载与Google pKey +摘要一起发送到rsa.VerifyPKCS1v15函数中。
你在这部分是错误的。你必须将编码后的头部和负载发送给RSA函数rsa.VerifyPKCS1v15
。
换句话说:你检查了签名{"alg":"RS256","kid":"d91c503452d0f8849200a321ffbf7dea76f9371d"}.{"iss":"accounts.google.com","sub":"104869993929250743503","azp":"client_email_till_@.apps.googleusercontent.com","email":"test@test.hr","at_hash":"KAm1M0g-ssMkdjds7jkbVQ","email_verified":true,"aud":client_email_till_@.apps.googleusercontent.com","hd":"test.hr","iat":1412246551,"exp":1412250451}
这是错误的。
你必须检查签名eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ5MWM1MDM0NTJkMGY4ODQ5MjAwYTMyMWZmYmY3ZGVhNzZmOTM3MWQifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0ODY5OTkzOTI5MjUwNzQzNTAzIiwiYXpwIjoiY2xpZW50X2VtYWlsX3RpbGxfQC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidGVzdEB0ZXN0LmhyIiwiYXRfaGFzaCI6IktBbTFNMGctc3NNa2RqZHM3amtiVlEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjpjbGllbnRfZW1haWxfdGlsbF9ALmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJ0ZXN0LmhyIiwiaWF0IjoxNDEyMjQ2NTUxLCJleHAiOjE0MTIyNTA0NTF9
的签名。
英文:
I send the **decoded** header.payload together with the Google pKey + digest to the rsa function rsa.VerifyPKCS1v15.
You are wrong in this part. You must send to the RSA function rsa.VerifyPKCS1v15
the encoded header.payload
In another words: you checked the signature of {"alg":"RS256","kid":"d91c503452d0f8849200a321ffbf7dea76f9371d"}.{"iss":"accounts.google.com","sub":"104869993929250743503","azp":"client_email_till_@.apps.googleusercontent.com","email":"test@test.hr","at_hash":"KAm1M0g-ssMkdjds7jkbVQ","email_verified":true,"aud":client_email_till_@.apps.googleusercontent.com","hd":"test.hr","iat":1412246551,"exp":1412250451}
which is wrong.
You must check the signature of eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ5MWM1MDM0NTJkMGY4ODQ5MjAwYTMyMWZmYmY3ZGVhNzZmOTM3MWQifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA0ODY5OTkzOTI5MjUwNzQzNTAzIiwiYXpwIjoiY2xpZW50X2VtYWlsX3RpbGxfQC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidGVzdEB0ZXN0LmhyIiwiYXRfaGFzaCI6IktBbTFNMGctc3NNa2RqZHM3amtiVlEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjpjbGllbnRfZW1haWxfdGlsbF9ALmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJ0ZXN0LmhyIiwiaWF0IjoxNDEyMjQ2NTUxLCJleHAiOjE0MTIyNTA0NTF9
.
答案4
得分: 1
要检查的输入应该是$base64_header.$base64_claim_set。
根据Google的文档:
JSON Web Signature (JWS) 是指导为JWT生成签名的规范。签名的输入是以下内容的字节数组:<br/>
{Base64url编码的头}.{Base64url编码的声明集}
我认为你可能只是为了演示而硬编码了证书索引。在你的真实代码中,你应该根据头部中的"kid"字段选择正确的证书。
英文:
The input to be checked should be the $base64_header.$base64_claim_set.
From Google's documentation:
> JSON Web Signature (JWS) is the specification that guides the
> mechanics of generating the signature for the JWT. The input for the
> signature is the byte array of the following content: <br/>
> {Base64url encoded header}.{Base64url encoded claim set}
I think you probably just hardcoded the cert index for demonstrating. In your real code, you should choose the correct cert based on the "kid" field in the header.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论