grpc.NewServer()
函数中,让我们添加一个新grpc.UnaryInterceptor()
选项。它期望一元服务器拦截器功能作为输入。
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Println("--> unary interceptor: ", info.FullMethod)
return handler(ctx, req)
}
func main() {
...
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
)
...
}
grpc.StreamInterceptor()
选项。server/main.go
文件中。func streamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Println("--> stream interceptor: ", info.FullMethod)
return handler(srv, stream)
}
User
结构体。它将包含 三个属性: username
、hashed_password
和role
type User struct {
Username string
HashedPassword string
Role string
}
NewUser()
函数来创建一个新用户,它接受用户名、密码和角色作为输入,并返回一个User
对象和一个错误
go get golang.org/x/crypto/bcrypt
func NewUser(username string, password string, role string) (*User, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("cannot hash password: %w", err)
}
user := &User{
Username: username,
HashedPassword: string(hashedPassword),
Role: role,
}
return user, nil
}
IsCorrectPassword
来检查给定的密码是否正确
bcrypt.CompareHashAndPassword()
函数,传入用户的哈希密码和给定的明文密码。函数返回true则证明一致否则不一致func (user *User) IsCorrectPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
return err == nil
}
func (user *User) Clone() *User {
return &User{
Username: user.Username,
HashedPassword: user.HashedPassword,
Role: user.Role,
}
}
service/user_store.go
文件UserStore
接口,它将有两个功能:
type UserStore interface {
Save(user *User) error
Find(username string) (*User, error)
}
InMemoryUserStore
结构体来实现接口
type InMemoryUserStore struct {
mutex sync.RWMutex
users map[string]*User
}
func NewInMemoryUserStore() *InMemoryUserStore {
return &InMemoryUserStore{
users: make(map[string]*User),
}
}
func (store *InMemoryUserStore) Save(user *User) error {
store.mutex.Lock()
defer store.mutex.Unlock()
if store.users[user.Username] != nil {
return fmt.Errorf("ErrAlreadyExists")
}
store.users[user.Username] = user.Clone()
return nil
}
func (store *InMemoryUserStore) Find(username string) (*User, error) {
store.mutex.RLock()
defer store.mutex.RUnlock()
user := store.users[username]
if user == nil {
return nil, nil
}
return user.Clone(), nil
}
service/jwt_manager.go
文件type JWTManager struct {
secretKey string
tokenDuration time.Duration
}
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
return &JWTManager{secretKey, tokenDuration}
}
go get github.com/dgrijalva/jwt-go
UserClaims
结构体。它将包含 JWTStandardClaims
作为复合字段。type UserClaims struct {
jwt.StandardClaims
Username string `json:"username"`
Role string `json:"role"`
}
Generate
来为特定用户生成并签署一个新的访问令牌
UserClaims
对象jwt.NewWithClaims()
函数来生成一个令牌对象,为简单起见,这里使用HS256
.func (manager *JWTManager) Generate(user *User) (string, error) {
claims := UserClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(manager.tokenDuration).Unix(),
},
Username: user.Username,
Role: user.Role,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(manager.secretKey))
}
jwt.ParseWithClaims()
,传入访问令牌,一个空的用户声明和一个自定义键函数。在这个函数中,检查令牌的签名方法以确保它与我们的服务器使用的算法匹配非常重要,在我们的例子中是 HMAC。UserClaims
对象func (manager *JWTManager) Verify(accessToken string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(
accessToken,
&UserClaims{},
func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, fmt.Errorf("unexpected token signing method")
}
return []byte(manager.secretKey), nil
},
)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}
proto/auth_service.proto
文件LoginRequest
包含 2 个字段的消息:一个 stringusername
和一个 string password
。然后是LoginResponse
一条只有 1 个字段的消息:access_token
.AuthService
.message LoginRequest {
string username = 1;
string password = 2;
}
message LoginResponse { string access_token = 1; }
service AuthService {
rpc Login(LoginRequest) returns (LoginResponse) {};
}
make gen
生成go代码service/auth_server.go
文件来实现这个新服务AuthServer
type AuthServer struct {
pb.UnimplementedAuthServiceServer
userStore UserStore
jwtManager *JWTManager
}
func NewAuthServer(userStore UserStore, jwtManager *JWTManager) *AuthServer {
return &AuthServer{
userStore: userStore,
jwtManager: jwtManager,
}
}
userStore.Find()
通过用户名查找用户jwtManager.Generate()
生成一个新的访问令牌func (server *AuthServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
user, err := server.userStore.Find(req.GetUsername())
if err != nil {
return nil, status.Errorf(codes.Internal, "cannot find user: %v", err)
}
if user == nil || !user.IsCorrectPassword(req.GetPassword()) {
return nil, status.Errorf(codes.NotFound, "incorrect username/password")
}
token, err := server.jwtManager.Generate(user)
if err != nil {
return nil, status.Errorf(codes.Internal, "cannot generate access token")
}
res := &pb.LoginResponse{AccessToken: token}
return res, nil
}
cmd/server/main.go
文件const (
secretKey = "secret"
tokenDuration = 15 * time.Minute
)
func main() {
laptopStore := service.NewInMemoryLaptopStore()
imageStore := service.NewDiskImageStore("img")
ratingStore := service.NewInMemoryRatingStore()
laptopServer := service.NewLaptopServer(laptopStore, imageStore, ratingStore)
//
userStore := service.NewInMemoryUserStore()
jwtManager := service.NewJWTManager(secretKey, tokenDuration)
authServer := service.NewAuthServer(userStore, jwtManager)
//
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
pb.RegisterLaptopServiceServer(grpcServer, laptopServer)
//
pb.RegisterAuthServiceServer(grpcServer, authServer)
//
reflection.Register(grpcServer)
listener, _ := net.Listen("tcp", ":8888")
grpcServer.Serve(listener)
}
func createUser(userStore service.UserStore, username, password, role string) error {
user, err := service.NewUser(username, password, role)
if err != nil {
return err
}
return userStore.Save(user)
}
seedUsers()
函数中,我调用该createUser()
函数 2 次以创建 1 个管理员用户和 1 个普通用户。假设他们有相同的secret
密码。func seedUsers(userStore service.UserStore) error {
err := createUser(userStore, "admin1", "secret", "admin")
if err != nil {
return err
}
return createUser(userStore, "user1", "secret", "user")
}
func main() {
userStore := service.NewInMemoryUserStore()
err := seedUsers(userStore)
if err != nil {
log.Fatal("cannot seed users: ", err)
}
jwtManager := service.NewJWTManager(secretKey, tokenDuration)
authServer := service.NewAuthServer(userStore, jwtManager)
laptopStore := service.NewInMemoryLaptopStore()
imageStore := service.NewDiskImageStore("img")
ratingStore := service.NewInMemoryRatingStore()
laptopServer := service.NewLaptopServer(laptopStore, imageStore, ratingStore)
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
pb.RegisterLaptopServiceServer(grpcServer, laptopServer)
pb.RegisterAuthServiceServer(grpcServer, authServer)
reflection.Register(grpcServer)
listener, _ := net.Listen("tcp", ":8888")
grpcServer.Serve(listener)
}
make server
evans -r -p 8888
service/auth_interceptor.go
文件AuthInterceptor
type AuthInterceptor struct {
jwtManager *JWTManager
accessibleRoles map[string][]string
}
NewAuthInterceptor()
函数来构建并返回一个新的身份验证拦截器对象func NewAuthInterceptor(jwtManager *JWTManager, accessibleRoles map[string][]string) *AuthInterceptor {
return &AuthInterceptor{jwtManager, accessibleRoles}
}
service/auth_interceptor.go
文件中实现新的一元拦截函数func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Println("--> unary interceptor: ", info.FullMethod)
// TODO: implement authorization
return handler(ctx, req)
}
}
Stream()
方法,该方法将创建并返回一个 gRPC 流服务器拦截器函数func (interceptor *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Println("--> stream interceptor: ", info.FullMethod)
// TODO: implement authorization
return handler(srv, stream)
}
}
cmd/server/main.go
文件中,我们必须使用 jwt 管理器和可访问角色的映射创建一个新的拦截器对象。在grpc.NewServer()
函数中,我们可以传入interceptor.Unary()
and interceptor.Stream()
。func accessibleRoles() map[string][]string {
const laptopServicePath = "/techschool.pcbook.LaptopService/"
return map[string][]string{
laptopServicePath + "CreateLaptop": {"admin"},
laptopServicePath + "UploadImage": {"admin"},
laptopServicePath + "RateLaptop": {"admin", "user"},
}
}
func main() {
...
interceptor := service.NewAuthInterceptor(jwtManager, accessibleRoles())
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(interceptor.Unary()),
grpc.StreamInterceptor(interceptor.Stream()),
)
...
}
authorize()
函数,它将以上下文和方法作为输入,如果请求未经授权,将返回错误。func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error {
// TODO: implement this
}
Unary()
函数和Stream()函数中,我们使用interceptor.authorize()
函数func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Println("--> unary interceptor: ", info.FullMethod)
err := interceptor.authorize(ctx, info.FullMethod)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
}
func (interceptor *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Println("--> stream interceptor: ", info.FullMethod)
err := interceptor.authorize(stream.Context(), info.FullMethod)
if err != nil {
return err
}
return handler(srv, stream)
}
}
authorize()
功能
metadata.FromIncomingContext()
以获取请求的元数据authorization
元数据键中获取值jwtManager.Verify()
以验证令牌并取回声明。nil
。如果没有,我们返回PermissionDenied
状态码,以及一条消息说用户没有访问这个 RPC 的权限func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error {
accessibleRoles, ok := interceptor.accessibleRoles[method]
if !ok {
// everyone can access
return nil
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Errorf(codes.Unauthenticated, "metadata is not provided")
}
values := md["authorization"]
if len(values) == 0 {
return status.Errorf(codes.Unauthenticated, "authorization token is not provided")
}
accessToken := values[0]
claims, err := interceptor.jwtManager.Verify(accessToken)
if err != nil {
return status.Errorf(codes.Unauthenticated, "access token is invalid: %v", err)
}
for _, role := range accessibleRoles {
if role == claims.Role {
return nil
}
}
return status.Error(codes.PermissionDenied, "no permission to access this RPC")
}
auth_client.go
文件AuthClient
调用身份验证服务的结构type AuthClient struct {
service pb.AuthServiceClient
username string
password string
}
AuthClient
对象func NewAuthClient(cc *grpc.ClientConn, username string, password string) *AuthClient {
service := pb.NewAuthServiceClient(cc)
return &AuthClient{service, username, password}
}
Login()
函数来调用 Login RPC 来获取访问令牌func (client *AuthClient) Login() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req := &pb.LoginRequest{
Username: client.username,
Password: client.password,
}
res, err := client.service.Login(ctx, req)
if err != nil {
return "", err
}
return res.GetAccessToken(), nil
}
client/auth_interceptor.go
,在调用服务器之前,我们将拦截所有 gRPC 请求并为它们附加访问令牌AuthInterceptor
结构type AuthInterceptor struct {
authClient *AuthClient
authMethods map[string]bool
accessToken string
}
NewAuthInterceptor()
函数中,除了 auth 客户端和 auth 方法之外,我们还需要一个刷新令牌持续时间参数。它会告诉我们应该多久调用一次登录 API 来获取新令牌。
scheduleRefreshToken()
调度刷新访问令牌并传入刷新持续时间func NewAuthInterceptor(authClient *AuthClient, authMethods map[string]bool, refreshDuration time.Duration) (*AuthInterceptor, error) {
interceptor := &AuthInterceptor{
authClient: authClient,
authMethods: authMethods,
}
err := interceptor.scheduleRefreshToken(refreshDuration)
if err != nil {
return nil, err
}
return interceptor, nil
}
scheduleRefreshToken()
功能
interceptor.accessToken
字段中。我们在这里写一个简单的日志,稍后观察,最后返回nil
。func (interceptor *AuthInterceptor) refreshToken() error {
accessToken, err := interceptor.authClient.Login()
if err != nil {
return err
}
interceptor.accessToken = accessToken
log.Printf("token refreshed: %v", accessToken)
return nil
}
func (interceptor *AuthInterceptor) scheduleRefreshToken(refreshDuration time.Duration) error {
err := interceptor.refreshToken()
if err != nil {
return err
}
go func() {
wait := refreshDuration
for {
time.Sleep(wait)
err := interceptor.refreshToken()
if err != nil {
wait = time.Second
} else {
wait = refreshDuration
}
}
}()
return nil
}
Unary()
函数来返回一个 gRPC 一元客户端拦截器。
func (interceptor *AuthInterceptor) Unary() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
log.Printf("--> unary interceptor: %s", method)
if interceptor.authMethods[method] {
return invoker(interceptor.attachToken(ctx), method, req, reply, cc, opts...)
}
return invoker(ctx, method, req, reply, cc, opts...)
}
}
attachToken()
函数来将令牌附加到输入上下文并返回结果。
metadata.AppendToOutgoingContext()
, 将输入上下文连同授权密钥和访问令牌值一起传递。func (interceptor *AuthInterceptor) attachToken(ctx context.Context) context.Context {
return metadata.AppendToOutgoingContext(ctx, "authorization", interceptor.accessToken)
}
func (interceptor *AuthInterceptor) Stream() grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
log.Printf("--> stream interceptor: %s", method)
if interceptor.authMethods[method] {
return streamer(interceptor.attachToken(ctx), desc, cc, method, opts...)
}
return streamer(ctx, desc, cc, method, opts...)
}
}
cc1
,并用它创建一个新的身份验证客户端authMethods()
来定义需要身份验证的方法列表。const (
username = "admin1"
password = "secret"
refreshDuration = 30 * time.Second
)
func authMethods() map[string]bool {
const laptopServicePath = "/techschool.pcbook.LaptopService/"
return map[string]bool{
laptopServicePath + "CreateLaptop": true,
laptopServicePath + "UploadImage": true,
laptopServicePath + "RateLaptop": true,
}
}
func main() {
cc1, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
authClient := client.NewAuthClient(cc1, username, password)
interceptor, _ := client.NewAuthInterceptor(authClient, authMethods(), refreshDuration)
}
func main() {
cc1, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
authClient := client.NewAuthClient(cc1, username, password)
interceptor, _ := client.NewAuthInterceptor(authClient, authMethods(), refreshDuration)
cc2, _ := grpc.Dial(
"localhost:8888",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(interceptor.Unary()),
grpc.WithStreamInterceptor(interceptor.Stream()),
)
laptopClient := pb.NewLaptopServiceClient(cc2)
testRateLaptop(laptopClient)
}
.PHONY: gen clean server client test
gen:
protoc --proto_path=proto --go_out=pb --go-grpc_out=pb proto/*.proto
clean:
rm pb/*.go
test:
go test -cover -race serializer/file_test.go
server:
go run cmd/server/main.go
client:
go run cmd/client/main.go
.PHONY: gen clean server client test
const (
username = "admin"
password = "secret"
refreshDuration = 30 * time.Second
)
在启动客户端发现无法正常工作,与服务器所规定的账户不一致
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(interceptor.Unary()),
grpc.WithStreamInterceptor(interceptor.Stream()),
)
laptopClient := pb.NewLaptopServiceClient(cc2)
testRateLaptop(laptopClient)
}
15. 向Makefile文件中添加一句话:`.PHONY: gen clean server client test`
```makefile
gen:
protoc --proto_path=proto --go_out=pb --go-grpc_out=pb proto/*.proto
clean:
rm pb/*.go
test:
go test -cover -race serializer/file_test.go
server:
go run cmd/server/main.go
client:
go run cmd/client/main.go
.PHONY: gen clean server client test
const (
username = "admin"
password = "secret"
refreshDuration = 30 * time.Second
)