在微服务架构下,不同服务之间会频繁发起内部调用,如果没有有效的权限控制机制,恶意服务或者异常服务可能会随意访问其他服务的敏感接口,引发数据泄露、业务异常等问题。用Golang实现服务间权限控制,核心思路是通过统一的身份凭证校验,确认调用方的合法身份与权限范围。

核心实现思路
服务间权限控制通常采用令牌机制,流程分为三个核心环节:
- 调用方服务在发起请求前,先向认证中心申请访问令牌,令牌中包含调用方身份、权限范围、有效期等信息
- 调用方将令牌放在请求头中传递给被调用服务
- 被调用服务接收到请求后,先校验令牌的合法性、有效期以及权限是否匹配,校验通过才处理请求
令牌生成实现
这里使用JWT作为令牌格式,Golang中可以通过github.com/golang-jwt/jwt/v5库实现令牌生成,首先需要定义令牌的声明结构体:
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
// 自定义令牌声明结构,继承jwt.RegisteredClaims
type ServiceClaims struct {
ServiceID string `json:"service_id"` // 调用方服务唯一标识
Permissions []string `json:"permissions"` // 调用方拥有的权限列表
jwt.RegisteredClaims
}
// 生成服务访问令牌
// secret为认证中心的签名密钥,expireHours为令牌有效期小时数
func GenerateServiceToken(serviceID string, permissions []string, secret string, expireHours int) (string, error) {
claims := ServiceClaims{
ServiceID: serviceID,
Permissions: permissions,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expireHours))),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "service-auth-center",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
令牌校验实现
被调用服务需要校验请求头中的令牌,确认调用方身份和权限是否合法,校验逻辑如下:
package auth
import (
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
// 校验服务令牌,返回解析后的声明和错误信息
func VerifyServiceToken(tokenString string, secret string) (*ServiceClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &ServiceClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*ServiceClaims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrInvalidKey
}
// 权限校验中间件,校验请求头中的令牌并验证是否拥有目标权限
// requiredPermission为访问当前接口需要的权限标识
func AuthMiddleware(secret string, requiredPermission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取Authorization字段
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "缺少访问令牌", http.StatusUnauthorized)
return
}
// 提取令牌,格式为Bearer {token}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "令牌格式错误", http.StatusUnauthorized)
return
}
tokenString := parts[1]
claims, err := VerifyServiceToken(tokenString, secret)
if err != nil {
http.Error(w, "令牌校验失败", http.StatusUnauthorized)
return
}
// 校验是否拥有所需权限
hasPermission := false
for _, p := range claims.Permissions {
if p == requiredPermission {
hasPermission = true
break
}
}
if !hasPermission {
http.Error(w, "无访问权限", http.StatusForbidden)
return
}
// 校验通过,继续处理请求
next.ServeHTTP(w, r)
})
}
}
服务调用示例
调用方服务在发起请求时,需要先获取令牌,再将令牌放入请求头,示例代码如下:
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
// 假设已经获取到令牌,实际场景中可以从本地缓存或者认证中心获取
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlX2lkIjoiYXBpLXNlcnZpY2UiLCJwZXJtaXNzaW9ucyI6WyJ1c2VyOmRldGFpbDpyZWFkIl0sImV4cCI6MTcxOTYwMDAwMCwiaXNzIjoic2VydmljZS1hdXRoLWNlbnRlciJ9.abc123"
// 发起服务调用请求
req, err := http.NewRequest("GET", "http://127.0.0.1:8080/api/user/detail", nil)
if err != nil {
fmt.Printf("创建请求失败: %vn", err)
return
}
// 设置令牌到请求头
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("请求失败: %vn", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("响应状态码: %d, 响应内容: %sn", resp.StatusCode, string(body))
}
注意事项
在实际落地时还需要注意几个问题:
- 令牌的签名密钥需要严格保密,不同环境可以使用不同的密钥,避免密钥泄露导致令牌被伪造
- 令牌有效期不宜设置过长,同时可以配合本地缓存减少每次请求都去认证中心校验的压力
- 权限标识需要统一规范,避免不同服务之间权限定义冲突,建议采用服务名:资源:操作的格式定义权限
- 如果是高并发场景,可以考虑使用分布式缓存存储令牌黑名单,支持令牌主动失效能力