Original link: Ewan Valentine. IO, translation is authorized by author Ewan Valentine.

The full code for this article: GitHub

The previous section introduced the user-service microservice and stored user data, including plaintext passwords, in Postgres. This section encrypts passwords securely and uses a unique token to identify users between microservices.

To begin, you need to run the database container manually:

$ docker run -d -p 5432:5432 postgres
$ docker run -d -p 27017:27017 mongo
Copy the code

Password hash processing

Security principle

Follow the principle that sensitive data such as passwords cannot be restored even in the event of a data breach. Never store passwords in plain text. Despite this claim, there are still projects that are plaintext storage, such as the old CSDN

Hash processing

Now update the password logic in user-service/handler.go to hash the password before storing it:

// user-service/hander.go func (h *handler) Create(ctx context.Context, req *pb.User, Resp * pb in the error Response) {/ / hash handle user input Password hashedPwd, err: = bcrypt. GenerateFromPassword (byte [] (the req. Password), bcrypt.DefaultCost) if err ! = nil { return err } req.Password = string(hashedPwd) if err := h.repo.Create(req); err ! = nil { return nil } resp.User = req return nil } func (h *handler) Auth(ctx context.Context, req *pb.User, resp *pb.Token) error { u, err := h.repo.GetByEmailAndPassword(req) if err ! = nil {return err} / / Password authentication if err: = bcrypt.Com pareHashAndPassword (byte [] (u.P assword), byte [] (the req. Password)); err ! = nil { return err } t, err := h.tokenService.Encode(u) if err ! = nil { return err } resp.Token = t return nil }Copy the code

There are only two changes: add password hashing logic to Create() and authentication with password hashing in Auth(). Create a new user with the following password:

Now that the user’s password authentication has been successfully completed with the database, there are many options for user authentication between multiple micro-services, and JWT is used in this paper

JWT

Introduction to the

JWT stands for JSON Web Tokens and is a distributed security protocol similar to OAuth. Simple enough to understand, the JWT algorithm generates a unique hash string for each user for checksum recognition. In addition, the user’s metadata (information data) can also be part of the encrypted string. Such as:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cB ab30RMHrHDcEfxjoYZgeFONFh7HgQCopy the code

You can see that the token is “. A string divided into three parts: header.paypay. signature

header

The JWT header contains two parts: declaring the token type and encrypting the token algorithm, which is base64 encrypted as the first part of the token:

{
  "typ": "JWT",
  "alg": "HS256"
}
Copy the code

Used to tell clients how to decode tokens

payload

Stores metadata, such as user data and token expiration time.

signature

The token is obtained after the header, payload, and key are encrypted and used for data verification on the client to ensure that the token is not changed during transmission. Stop using JWT for Sessions Stop using JWT for Sessions

I suggest that you add the IP address that creates user information to the payload to generate tokens. In this way, you can effectively prevent others from stealing the token and forging the identity of the user on other devices. In addition, HTTPS can be used for encrypted transmission to avoid man-in-the middle attack.

JWT hash algorithms can be roughly divided into two categories, symmetric encryption and asymmetric encryption. The former uses the same private key for encryption and decryption, while the latter uses public key encryption and private key decryption. Asymmetric encryption algorithm is more used for authentication between micro services. Check out JWT Signing Algorithms Overview, JSON Web Algorithms

use

We use the third party open source library: Dgrijalva/JwT-go, using the sample direct reference documentation.

JWT encryption and decryption

Generate JWT token string based on user information and modify user-service/token_service.go

// user-service/token_service.go package main import (...) type Authable interface { Decode(tokenStr string) (*CustomClaims, error) Encode(user *pb.User) (string, Var privateKey = []byte(" 'xs#a_1-!")} // use md5 to generate privateKey = []byte("' xs#a_1-! ) // Custom metadata, Type CustomClaims struct {User *pb.User // Uses the standard payload jwt.StandardClaims} type TokenService Func (SRV *TokenService) Decode(tokenStr string) (*CustomClaims, error) { t, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, Error) {return privateKey, nil}) // Decrypt the converted type and return if claims, ok := t.claims.(*CustomClaims); ok && t.Valid { return claims, nil } else { return nil, Func (SRV *TokenService) Encode(User * pb.user) Error) {// expireTime := time.now ().add (time.hour * 24 * 3).UNIX () claims := CustomClaims{user, Jwt. StandardClaims{Issuer: "go.micro-srv. user", // Issuer: ExpiresAt: expireTime, }, } jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return jwtToken.SignedString(privateKey) }Copy the code

In our code we use privateKey as the salt for JWT encryption, so be sure to use a more secure value in production and keep it safe. The comments on the above code are more detailed and roughly say:

Decode() takes a string argument, parses it into a JWT token object, and verifies that the information is user information. If so, metadata is retrieved to obtain user information.

Encode() takes a user metadata data, hases it as a JWT token string and returns it.

Token is generated

Now that we have a service that validates tokens, let’s update the user-CLI code. In this case, just to run through the token_service validation process, the user data is written in the code first.

package main import (...) Client := pb.newUserServiceclient (" go.micro-srv.user ", Microclient. DefaultClient) / / temporary user information write to death in the code name: = "Ewan Valentine email:" = "[email protected]" password: = "test123" company := "BBC" resp, err := client.Create(context.TODO(), &pb.User{ Name: name, Email: email, Password: password, Company: company, }) if err ! = nil { log.Fatalf("call Create error: %v", err) } log.Println("created: ", resp.User.Id) allResp, err := client.GetAll(context.Background(), &pb.Request{}) if err ! = nil { log.Fatalf("call GetAll error: %v", err) } for _, u := range allResp.Users { log.Printf("%v\n", u) } authResp, err := client.Auth(context.TODO(), &pb.User{ Email: email, Password: password, }) if err ! Log.fatalf ("auth failed: %v", err)} log.println ("token: ", authresp.token) // Exit(0)}Copy the code

After making a make build for both user-service and user-cli, the token is generated as follows:

Save this token for later use.

Token authentication

Now there is logic to generate token for the user, which can be used in ffuel-service micro-service to identify the user between ffuel-CLI and ffuel-service through token.

// consignment-cli/cli.go // ... Func main() {cmd.init () // Create a microservice client, Simplifies the manual Dial connection server client step: = pb. NewShippingServiceClient (" go. Micro. The SRV. Consignment ", Microclient. DefaultClient) / / specified on the command line new cargo information json pieces if len (OS. The Args) < 3 {the Fatalln (" Not in the arguments, Expecing file and token.")} infoFile := Os. Args[1] Token := os.Args[2] err := parseFile(infoFile) if err ! = nil { log.Fatalf("parse info file error: %v", err)} // Create a context with the user's token // fPC-service the server will fetch the token from it, TokenContext := metadata.NewContext(context.background (), map[string]string{"token": Resp, err := client.createconsignment (tokenContext, consignment) if err! = nil { log.Fatalf("create consignment error: %v", err) } log.Printf("created: Resp = client.GetConsignments(tokenContext, &pb.getrequest {}) if err! = nil { log.Fatalf("failed to list consignments: %v", err) } for i, c := range resp.Consignments { log.Printf("consignment_%d: %v\n", i, c) } }Copy the code

Alter fpc-service to listen to the request, fetch the token from it and call user-service for validation:

package main import (...) Const (DEFAULT_HOST = "127.0.0.1:27017") func main() {//... SRV := micro.NewService(// must be the same as the package in fPA. Proto micro.Name("go.micro. micro.Version("latest"), micro.WrapHandler(AuthWrapper), ) // ... } // AuthWrapper is a high-order function, the input parameter is a "next" function, the output parameter is an authentication function // After the authentication logic is processed inside the returned function, then the next step is manually called fn() // The token is fetched from the ffM-CI context, // If the user is authenticated, fn() continues to execute. Func AuthWrapper(fn server.handlerfunc) server.handlerfunc {return func(CTX context.Context, req server.request, resp interface{}) error { meta, ok := metadata.FromContext(ctx) if ! ok { return errors.New("no auth meta-data found in request") } // Note this is now uppercase (not entirely sure why this  is...) token := meta["Token"] // Auth here authClient := userPb.NewUserServiceClient("go.micro.srv.user", client.DefaultClient) authResp, err := authClient.ValidateToken(context.Background(), &userPb.Token{ Token: token, }) log.Println("Auth Resp:", authResp) if err ! = nil { return err } err = fn(ctx, req, resp) return err } }Copy the code

Make build again in fill-fence-service and fill-fence-CLI respectively for the modification to take effect. Rebuild the image:

$ docker run --net="host" \
	-e MICRO_REGISTRY=mdns \
	consignment-cli consignment.json \
	<TOKEN_HERE>
Copy the code

–net=”host” is used to specify that the container is running on the host’s local network, such as 127.0.0.1 or localhost, rather than on docker’s own internal network. In this case, you do not need to perform port mapping. You can use -p 8080 instead of -p 8080:8080. More references: Docker manual

Now run the above command and you will see that a new shipment has been created with the following effect:

If you delete several characters from the token, you will receive an authentication error similar to illegal base64 data at input Byte 41.

So far, we have implemented JWT encryption and decryption in user-service and used it as a mid-level authentication user between fill-fill-CLI and fill-fill-service. The whole operation process is as follows:

So before running, run user-service and vessel-service as well. It can also be seen that cargo ship and cargo data are stored successfully in MongoDB:

GRPC implementation

If you are still using gRPC instead of Go-Mirco, your authentication middleware should be implemented as follows:

func main() { ... myServer := grpc.NewServer( grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(AuthInterceptor), ) ... } func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // Set up a connection to the server. conn, err := grpc.Dial(authAddress, grpc.WithInsecure()) if err ! = nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewAuthClient(conn) r, err := c.ValidateToken(ctx, &pb.ValidateToken{Token: token}) if err ! = nil { log.Fatalf("could not authenticate: %v", err) } return handler(ctx, req) }Copy the code

Call switching

In addition, we don’t need to run all microservices locally; they should be independent of each other and can be tested in an isolated environment. For example, in the current project, if only the fpc-service’s own RPC is tested, there is no need to call user-service for authentication. I think it is better to switch whether to call other services in the code.

Update fpc-service authentication middleware:

// shippy-user-service/main.go ... func AuthWrapper(fn server.HandlerFunc) server.HandlerFunc { return func(ctx context.Context, req server.Request, Resp interface{}) error {// fPC-service independent test does not authenticate if os.Getenv("DISABLE_AUTH") == "true" {return fn(CTX, req, resp) } ... }}Copy the code

This can be set in our Makefile:

// shippy-user-service/Makefile ... run: docker run -d --net="host" \ -p 50052 \ -e MICRO_SERVER_ADDRESS=:50052 \ -e MICRO_REGISTRY=mdns \ -e DISABLE_AUTH=true \  consignment-serviceCopy the code

I find it easiest to use environment variables to decide whether to call other services so that your microservices can be tested in isolation.

conclusion

In this section, the password is hashed and stored, and JWT is introduced to encrypt and decrypt user data, and the token generated by JWT is used to authenticate user identity between micro-services. From Section 1 to Section 4, we realized ffm-service, vessel-Service and user-service as a complete system, and completed the basic functions of the cargo management system, which will be improved in the following chapters. The next section uses Go-Micro’s NATS plug-in for messaging.