2021SC@SDUSC
引言在sduoj项目中,我们有四种角色:管理员、教师、学生、普通用户。如果仅仅是通过调用的接口路径的不同来区分这些角色的话,容易引发一些危险的行为。比如,当一个普通用户知道了该项目的管理员接口,那他的行为就有可能造成系统的混乱。因此,我们需要给项目加一点防御,对接口进行访问控制。
JWT 的全称是 JSON WEB TOKEN,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT 的安装比较简单,输入以下命令即可:
go get -u github.com/dgrijalva/jwt-go
虽然jwt-go库能够对 JWT 令牌的相关行为进行比较快捷的处理,但是为了方便调用,我们还需要对它进一步封装。
源码分析 ClaimsChaims结构体中的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
}



