2021SC@SDUSC
引言
在sduoj项目中,我们有四种角色:管理员、教师、学生、普通用户。如果仅仅是通过调用的接口路径的不同来区分这些角色的话,容易引发一些危险的行为。比如,当一个普通用户知道了该项目的管理员接口,那他的行为就有可能造成系统的混乱。因此,我们需要给项目加一点防御,对接口进行访问控制。
JWT 的全称是 JSON WEB TOKEN,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT 的安装比较简单,输入以下命令即可:
go get -u github.com/dgrijalva/jwt-go
虽然jwt-go
库能够对 JWT 令牌的相关行为进行比较快捷的处理,但是为了方便调用,我们还需要对它进一步封装。
源码分析
Claims
Chaims
结构体中的AppKey
和AppSecret
是我们自定义的认证信息,而jwt.StandardClaims
结构体则是jwt-go
中定义的。
type Claims struct {
AppKey string `json:"app_key"`
AppSecret string `json:"app_secret"`
jwt.StandardClaims
}
我们知道,JWT 由三部分构成,第一部分是 header,第二部分为 payload,第三部分是 signature。
在 header 中存放着令牌类型和令牌使用的加密算法。
在 payload 存放有效信息,这些有效信息包含三个部分:标准中注册的声明、共有的声明和私有的声明。jwt.StandardClaims
定义的就是标准中注册的声明。
在signature存放签证信息,用于校验消息在整个过程中有没有被篡改。
jwt.SandardClaims
结构体中,Audience
是受众,即接受 JWT 的一方,ExpiresAt
是所签发的 JWT 过期时间,Id
是 JWT 的唯一标识,IssueAt
是签发时间,Issuer
是 JWT 的签发者,NotBefore
是 JWT 的生效时间,Subject
是主题。
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
GetJWTSecret
GetJWTSecret
用于从配置文件中获取该项目的 JWT 密钥,并将它转换成byte
数组。
func GetJWTSecret() []byte {
return []byte(global.JWTSetting.Secret)
}
GenerateToken
GenerateToken
方法用于生成 JWT Token,它利用参数中传入的appKey
、appSecret
,以及配置文件中的Issuer
(签发者)、Expire
(有效时间),根据指定的算法生成签名后的 Token。
func GenerateToken(appKey, appSecret string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(global.JWTSetting.Expire)
claims := Claims{
AppKey: util.EncodeMD5(appKey),
AppSecret: util.EncodeMD5(appSecret),
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: global.JWTSetting.Issuer,
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(GetJWTSecret())
return token, err
}
time.Now
可以获取当前时间,用这个时间加上 Token 的有效时间Expire
,得到过期时间expireTime
,再利用Unix
方法,得到一个int64
类型的、从时间点 January 1, 1970 UTC 到时间点t所经过的时间(单位 s)。
func (t Time) Unix() int64 {
return t.unixSec()
}
参数中的appKey
和appSecret
并没有直接传入Claims
结构体,而是经过了 MD5 加密。
func EncodeMD5(value string) string {
m := md5.New()
m.Write([]byte(value))
return hex.EncodeToString(m.Sum(nil))
}
NewWithClaims
会根据加密算法和Claims
对象来创建Token
实例,这个实例中的Header
就是之前提到的 JWT 三部分之一。
func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: claims,
Method: method,
}
}
signedString
方法会利用传入的密钥生成签名字符串。它利用t.SigningString
与t.Method.Sign
返回的字符串以.
为分隔符拼装在一起并返回。
func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string
var err error
if sstr, err = t.SigningString(); err != nil {
return "", err
}
if sig, err = t.Method.Sign(sstr, key); err != nil {
return "", err
}
return strings.Join([]string{sstr, sig}, "."), nil
}
SigningString
会将 header(头部)和 payload(荷载)部分做一次 base64Url 编码,在下面的代码中,parts
用于盛放编码后的字符串,最后利用strings.Join
将这两个字符串以.
为分隔符连接在一起。
func (t *Token) SigningString() (string, error) {
var err error
parts := make([]string, 2)
for i, _ := range parts {
var jsonValue []byte
if i == 0 {
if jsonValue, err = json.Marshal(t.Header); err != nil {
return "", err
}
} else {
if jsonValue, err = json.Marshal(t.Claims); err != nil {
return "", err
}
}
parts[i] = EncodeSegment(jsonValue)
}
return strings.Join(parts, "."), nil
}
t.Method.Sign
利用它的签名算法(这里是jwt.SigningMethodHS256
)、t.SigningString
得到的字符串、密钥secret
,生成一个签名字符串,即 JWT 三部分之一的 signature(签名)。
由此可以看出,签名是由头部、荷载、密钥、加密算法共同生成的,因此可以用来校验消息是否被篡改,一旦被篡改,签名就无法对上。
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() {
return "", ErrHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
return EncodeSegment(hasher.Sum(nil)), nil
}
return "", ErrInvalidKeyType
}
ParseToken
ParseToken
是GenerateToken
的反过程,它用来解析和校验 Token。该方法调用jwt.ParseWithClaims
获取tokenClaims
,然后对它进行格式的校验,并检查它是否有效,最终将Claims
返回。
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return GetJWTSecret(), nil
})
if tokenClaims != nil {
claims, ok := tokenClaims.Claims.(*Claims)
if ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
ParseWithClaims
用于解析鉴权的声明,它调用Parser.ParseWithClaims
进行解码和校验,并返回*Token
。
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
}
Token
结构体如下图所示,其中的Valid
用于表示该 Token 是否有效,它的值与ExpiresAt
、Issuer
、Not Before
有关。
type Token struct {
Raw string
Method SigningMethod
Header map[string]interface{}
Claims Claims
Signature string
Valid bool
}