经过上一篇文章的简单介绍,我们已经了解了目前常见的统一认证与鉴权的方案,接下来我们将基于 OAuth2 协议和 JWT 实现一套简单的认证和授权系统。系统主要由两个服务组成,授权服务器和资源服务器,它们之间的交互图 11-4 所示:
下面具体介绍 TokenGrant 令牌生成器以及 TokenService 令牌服务中主要的接口实现。
我们来看 TokenGranter 接口,它用于根据不同的授权类型进行不同的用户和客户端信息认证,并生成访问令牌返回,接口定义如下:
type TokenGranter interface {
grant(grantType string, client *ClientDetails, reader *http.Request) (*OAuth2Token, error)
}
TokenGranter 接受授权类型、请求的客户端和请求体作为参数。我们可以使用组合模式,使得不同的授权类型使用不同的 TokenGranter 实现来生成访问令牌,组合节点 ComposeTokenGranter 的定义如下:
type ComposeTokenGranter struct {
tokenGrantDict map[string] TokenGranter
}
func (tokenGranter *ComposeTokenGranter) grant(grantType string, client *ClientDetails, reader *http.Request) (*OAuth2Token, error) {
// 获取具体的授权 TokenGranter 生成访问令牌
dispatchGranter := tokenGranter.tokenGrantDict[grantType]
if dispatchGranter == nil{
return nil, errors.New("Grant Type " + grantType + " is not supported")
}
return dispatchGranter.grant(grantType, client, reader)
}
如上述代码所示,ComposeTokenGranter 中主要根据 grantType 获取具体的 TokenGranter 实现,并委托其验证客户端和用户凭证,从而生成访问令牌。比如在客户端使用密码类型请求访问令牌,那我们需要对客户端携带的用户名和密码进行校验,如 UsernamePasswordTokenGranter 密码类型的 TokenGranter 的代码所示:
func (tokenGranter *UsernamePasswordTokenGranter) grant(grantType string, client *ClientDetails, reader *http.Request) (*OAuth2Token, error) {
if grantType != tokenGranter.supportGrantType{
return nil, errors.New("Target Grant Type is " + grantType + ", but current grant type is " + tokenGranter.supportGrantType)
}
// 从请求体中获取用户名密码
username := reader.FormValue("username")
password := reader.FormValue("password")
if username == "" || password == ""{
return nil, errors.New( "Please provide correct user information")
}
// 验证用户名密码是否正确
userDetails, err := userDetailsService.GetUserDetailByUsername(username)
if err != nil{
return nil, errors.New( "Username "+ username +" is not exist")}
if !userDetails.IsMatch(username, password){
return nil, errors.New( "Username or password is not corrent")
}
// 根据用户信息和客户端信息生成访问令牌
return tokenGranter.tokenService.CreateAccessToken(&OAuth2Details{
Client:client,
User:userDetails,
})
}
在 UsernamePasswordTokenGranter#grant 方法中,主要进行了以下工作:
同时我们把令牌令牌刷新的相关的逻辑封装到 RefreshTokenGranter 中,代码如下:
func (tokenGranter *RefreshTokenGranter) grant(grantType string, client *ClientDetails, reader *http.Request) (*OAuth2Token, error) {
if grantType != tokenGranter.supportGrantType{
return nil, errors.New("Target Grant Type is " + grantType + ", but current grant type is " + tokenGranter.supportGrantType)
}
// 从请求中获取刷新令牌
refreshTokenValue := reader.URL.Query().Get("token")
if refreshTokenValue == ""{
return nil, errors.New("Please input Refresh Token")
}
return tokenService.RefreshAccessToken(refreshTokenValue)
}
在上述代码中,RefreshTokenGranter 将请求参数的中 refresh_token 取出,并调用 TokenService#RefreshAccessToken 根据刷新令牌请求访问令牌。
除了以上提供的 UsernamePasswordTokenGranter 和 RefreshTokenGranter 令牌生成器,读者们还可以根据需要实现其他授权类型的令牌生成器,甚至自定义标准授权类型以外的令牌生成器
在 TokenGrant 中,我们最后都是使用 TokenService#CreateAccessToken 为请求的客户端生成访问令牌。TokenService 用于生成和管理令牌,它使用 TokenStore 保存令牌,主要提供以下方法:
// 根据用户信息和客户端信息生成访问令牌
func (tokenService *TokenService) CreateAccessToken(oauth2Details *OAuth2Details) (*OAuth2Token, error);
// 根据刷新令牌获取访问令牌
func (tokenService *TokenService) RefreshAccessToken(refreshTokenValue string) (*OAuth2Token, error);
// 根据用户信息和客户端信息获取已生成访问令牌
func (tokenService *TokenService) GetAccessToken(details *OAuth2Details) (*OAuth2Token, error);
// 根据访问令牌值获取访问令牌结构体
func (tokenService *TokenService) ReadAccessToken(tokenValue string) (*OAuth2Token, error);
// 根据访问令牌获取对应的用户信息和客户端信息
func (tokenService *TokenService) GetOAuth2DetailsByAccessToken(tokenValue string) (*OAuth2Details, error);
由于代码较多,我们主要讲解 CreateAccessToken、GetOAuth2DetailsByAccessToken 和 RefreshAccessToken 方法,其他方法的实现读者可以在 ch11-security/token_service.go 中阅读。
CreateAccessToken 方法顾名思义是用来生成访问令牌。在该方法中,会尝试根据用户信息和客户端信息从 TokenStore 中获取已保存的访问令牌。如果访问令牌存在且为未失效,将会直接访问该访问令;如果访问令牌已经失效,那么将尝试根据用户信息和客户端信息生成一个新的访问令牌并返回。代码如下所示:
func (tokenService *TokenService) CreateAccessToken(oauth2Details *OAuth2Details) (*OAuth2Token, error) {
existToken, err := tokenService.tokenStore.GetAccessToken(oauth2Details)
var refreshToken *OAuth2Token
if err == nil{
// 存在未失效访问令牌,直接返回
if !existToken.IsExpired(){
tokenService.tokenStore.StoreAccessToken(existToken, oauth2Details)
return existToken, nil
}
// 访问令牌已失效,移除
tokenService.tokenStore.RemoveAccessToken(existToken.TokenValue)
if existToken.RefreshToken != nil {
refreshToken = existToken.RefreshToken
tokenService.tokenStore.RemoveRefreshToken(refreshToken.TokenType)
}
}
if refreshToken == nil || refreshToken.IsExpired(){
refreshToken, err = tokenService.createRefreshToken(oauth2Details)
if err != nil{
return nil, err
}
}
// 生成新的访问令牌
accessToken, err := tokenService.createAccessToken(refreshToken, oauth2Details)
if err == nil{
// 保存新生成令牌
tokenService.tokenStore.StoreAccessToken(accessToken, oauth2Details)
tokenService.tokenStore.StoreRefreshToken(refreshToken, oauth2Details)
}
return accessToken, err
}
在上述代码中,除了生成访问令牌,我们还生成了刷新令牌。在令牌生成成功之后,我们通过 TokenStore 将它们保存到系统中。生成访问令牌和刷新令牌的具体方法如下:
func (tokenService *TokenService) createAccessToken(refreshToken *OAuth2Token, oauth2Details *OAuth2Details) (*OAuth2Token, error) {
// 根据客户端信息计算有效时间
validitySeconds := oauth2Details.Client.AccessTokenValiditySeconds
s, _ := time.ParseDuration(strconv.Itoa(validitySeconds) + "s")
expiredTime := time.Now().Add(s)
accessToken := &OAuth2Token{
RefreshToken:refreshToken,
ExpiresTime:&expiredTime,
TokenValue:uuid.NewV4().String(),
}
// 转化访问令牌的类型
if tokenService.tokenEnhancer != nil{
return tokenService.tokenEnhancer.Enhance(accessToken, oauth2Details)
}
return accessToken, nil
}
func (tokenService *TokenService) createRefreshToken(oauth2Details *OAuth2Details) (*OAuth2Token, error) {
// 根据客户端信息计算有效时间
validitySeconds := oauth2Details.Client.RefreshTokenValiditySeconds
s, _ := time.ParseDuration(strconv.Itoa(validitySeconds) + "s")
expiredTime := time.Now().Add(s)
refreshToken := &OAuth2Token{
ExpiresTime:&expiredTime,
TokenValue:uuid.NewV4().String(),
}
// 转化授权令牌令牌的类型
if tokenService.tokenEnhancer != nil{
return tokenService.tokenEnhancer.Enhance(refreshToken, oauth2Details)
}
return refreshToken, nil
}
生成访问令牌和刷新令牌的方法大同小异,我们使用 UUID 来生成一个唯一的标识来区分不同的访问令牌和刷新令牌,并根据客户端信息中提供的访问令牌和刷新令牌的有效时长计算令牌的有效时间,最后还使用可能存在的 TokenEnhancer 来进行令牌样式的状态。后面的讲解中我们会使用 JwtTokenEnhancer 将当前的令牌转化为 JWT 样式。
生成访问令牌是与请求的客户端和用户信息相绑定,在验证访问令牌的有效性时,可以根据访问令牌逆向获取到客户端信息和用户信息,这样才能通过访问令牌确定当前的操作用户和委托的客户端。主要实现位于 GetOAuth2DetailsByAccessToken 方法中:
func (tokenService *TokenService) GetOAuth2DetailsByAccessToken(tokenValue string) (*OAuth2Details, error) {
accessToken, err := tokenService.tokenStore.ReadAccessToken(tokenValue)
if err == nil{
if accessToken.IsExpired(){
return nil, errors.New("Access Token " + tokenValue + " is Expired")
}
return tokenService.tokenStore.ReadOAuth2Details(tokenValue)
}
return nil, err
}
GetOAuth2DetailsByAccessToken 方法首先根据访问令牌的值从 TokenStore 中获取到对应的访问令牌结构体。如果访问令牌没有失效,再通过 TokenStore 获取生成访问令牌时绑定的用户信息和客户端信息。
RefreshAccessToken 方法用于根据刷新令牌生成新的访问令牌,通常在访问令牌失效时,客户端使用访问令牌中携带的刷新令牌重新生成新的有效访问令牌,代码如下所示:
func (tokenService *TokenService) RefreshAccessToken(refreshTokenValue string) (*OAuth2Token, error){
refreshToken, err := tokenService.tokenStore.ReadRefreshToken(refreshTokenValue)
if err == nil{
if refreshToken.IsExpired(){
return nil, errors.New("Refresh Token " + refreshTokenValue + " is Expired")
}
oauth2Details, err := tokenService.tokenStore.ReadOAuth2DetailsForRefreshToken(refreshTokenValue)
if err == nil{
oauth2Token, err := tokenService.tokenStore.GetAccessToken(oauth2Details)
// 移除原有的访问令牌
if err == nil{
tokenService.tokenStore.RemoveAccessToken(oauth2Token.TokenValue)
}
// 移除已使用的刷新令牌
tokenService.tokenStore.RemoveRefreshToken(refreshTokenValue)
refreshToken, err = tokenService.createRefreshToken(oauth2Details)
if err == nil{
accessToken, err := tokenService.createAccessToken(refreshToken, oauth2Details)
if err == nil{
tokenService.tokenStore.StoreAccessToken(accessToken, oauth2Details)
tokenService.tokenStore.StoreRefreshToken(refreshToken, oauth2Details)
}
return accessToken, err;
}
}
}
return nil, err
}
在上述代码中,我们首先使用 TokenStore 将刷新令牌值对应的刷新令牌结构体查询出来,用于判断刷新令牌是否过期。再根据刷新令牌值获取刷新令牌绑定的用户信息和客户端信息,最后我们移除已使用的刷新令牌,并根据用户信息和客户端信息生成新的刷新令牌和访问令牌返回。
TokenStore 负责存储生成的令牌和维护令牌、用户、客户端之间的绑定关系。
除此之外是对外提供的接口:/oauth/token 和 /oauth/check_token。分别用于获取授权的令牌和校验令牌。
这部分的实现不是很负责,读者可以根据笔者提供的思路自行尝试。具体的实现细节可以参见笔者出版的 《Go 语言高并发与微服务》一书。