برای احراز هویت اپلیکیشن خودمون قرار از JWT استفاده کنیم، برای این کار میریم سراغ سایت jwt.io و لایبرریهای گولنگ و تو نگاه اول SermoDigital/jose از همه کاملتر به نظر میرسه، داکیومنت نداره ولی فایل تست خوبی داره با یه نگاه به این تست میشه کامل درک کرد و هرچی برای ساخت توکن نیاز داریم اینجا پیدا میکنیم. (برای لایبرریهای کوچیک، خوندن کد سریعتر از خوندن داکیومنته)
برای شروع پکیج به فایل گلاید اضافه میکنیم و دستور glide update میزنیم. مرحله بعد دوتا روت برای ایجاد توکن و اعتبارسنجی توکن مینویسیم:
package main import "github.com/gin-gonic/gin" func setupRouter() *gin.Engine { router := gin.Default() router.GET("/v1/auth/login", loginController) router.GET("/v1/auth/user", parseToken) router.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "Hello world!", }) }) return router }
حالا فایل login.controller.go ایجاد میکنیم و دوتا فانکشن مربوطه توش مینویسیم:
package main import ( "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" "github.com/gin-gonic/gin" "strconv" "strings" "time" "os" ) var claims = jws.Claims{ "user": struct { Name, Email string }{ Name: "ErFUN KH", Email: "me@example.com", }, } func loginController(c *gin.Context) { exp, _ := strconv.Atoi(os.Getenv("JWT_EXPIRATION")) claims.SetExpiration(time.Now().Add(time.Duration(60 * 60 * 24 * time.Duration(exp) * time.Second))) claims.SetIssuer(os.Getenv("JWT_ISSUER")) claims.SetAudience(os.Getenv("JWT_AUDIENCE")) claims.SetIssuedAt(time.Now()) claims.SetNotBefore(time.Now()) claims.SetSubject("1") // set user id claims.SetJWTID("123") // set token id rsaPrivate, err := crypto.ParseRSAPrivateKeyFromPEM([]byte(os.Getenv("JWT_PRIVATE_KEY"))) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } jwt := jws.NewJWT(claims, crypto.SigningMethodRS256) token, err := jwt.Serialize(rsaPrivate) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } c.JSON(200, gin.H{ "token": string(token), }) } func parseToken(c *gin.Context) { rsaPublic, _ := crypto.ParseRSAPublicKeyFromPEM([]byte(os.Getenv("JWT_PUBLIC_KEY"))) jwt, err := jws.ParseJWTFromRequest(c.Request) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } if err = jwt.Validate(rsaPublic, crypto.SigningMethodRS256); err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } c.JSON(200, gin.H{ "data": jwt.Claims(), }) }
اول از همه یه شی claims ساختیم و توش نام و ایمیل کاربر وارد کردیم (فعلا هاردکد کردیم بعدا از دیتابیس میخونیم)، اینا دیتاهایی هستند که میخوایم سمت فرانتاند ازشون استفاده کنیم، بعدا بیشترش میکنیم ولی برای درک مطلب کافیه.
مرحله بعد تابع لاگین تعریف کردیم، خط اولش از فایل .env مدت منقضی شدن توکن خوندیم، مرحله بعد تاریخ منقضی شدن برای توکن تعریف کردیم، خط بعد صادر کننده توکن معرفی کردیم که البته اینم از متغییرها خوندیم، خط بعد سرویسی که قراره از این توکن استفاده کنه و همینطور به ترتیب: تاریخ ایجاد توکن، زمان امکان استفاده توکن بعد اون آیدی یوزر و آیدی توکن فعلا به صورت هاردکد تعریف کردیم تا بعدا درستش کنیم.
خط بعدی کلید خصوصی از فایل .env خوندیم و به لایبرری معرفی کردیم، بعد اون اگر خطایی نداد با استفاده از دیتاهایی که داشتیم و کلید خصوصی شئ jwt ایجاد میکنیم، بعد اون از روی شئ jwt توکن ایجاد میکنیم و برای کاربر ارسال میکنیم.
خب تابع بعدی اول کلید عمومی خوندیم بعد سعی کردیم توکن از هدر بگیریم، شما باید توکن به این صورت تو هدر ریکوئست وارد کنید:
Authorization: Bearer YOUR_TOKEN
بعد اون سعی کردیم توکن با کلید عمومی چک کنیم تا مطمئن بشیم خودمون ایجادش کردیم، حالا اگه مشکلی نبود اطلاعات توکن باز میکنیم و برای خودمون برمیگردونیم تا مطمئن بشیم کد به درستی کار میکنه.
یادمون نمیره متغییرهای جدید به فایل .env اضافه کنیم:
# JWT config JWT_PRIVATE_KEY='-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA2wo4ugkqLamtl+6AxgCg86qAkuVGsh/dQ7SJ1Kb0sLOFrfsB\nF8SKagQB3JkwB2cTRzD6WyvcvFEGLo4KJAa/U9xfMlyCyMCDRCeabCzNsNPKOL5t\nEW8120X0AERc1g9mRQGwr/uMsBmlwGzutqutB/CXKL7sIw0yynJzlltTI+ISGMcb\nB8aDAOSSuR1hcZCCk3Jaat9M2DdK4/lYmVO/IFYdGgYLlItRqHExKmZInJASHIur\nquxgI72OVq4jXhVCARBlLaQa25oPWcp94+OgTS0wYvQl1aE9H9rok9bSd/YES3kL\nkH3DdjNRPzSRRCX+J8CxUFZw6wFXsVmpCtEM3QIDAQABAoIBADrDXUCboNMrSEUQ\nWT/Ff2iff2rpU7QJ1GSLlMaWG+Mj5mMsibiEo9WZSZ6TAk2aG5Pn0eKPu+JRomTu\n+k17+exXnLp4EyYkb5LjRQxsYKplx0S94ajhuwMemz1PGdDbxMYSlAJCbBX6a3ta\nPhiHqh4NL6BgyB0HN28UkWnvCjj/uFUz6qgJQFZMs1qx7LRHhPkPIFx/GVNng8qz\nEw7pXsgMuTysOmuBO97By95eDqNYNJYqQwGodIhG7zxbyxXGnT3xwOoXN2L68XpR\npQh5Nl5BgpPLOirJUW+O2abUi/i+bHOIozS9DKyGtm+20yiCSIOzEcnZ0rdMQDYw\nL4W+wMECgYEA9Rdg1R5lJF/+uLsuk87rsLEt5bWiqEleihGf/qDkurOe1trbnlZ2\nYBHEuObykJSq4wZpM0zOyjycNYfQ/zteuxnEEc+NIYdmN3KGxflL5jBAEjyYQo4a\nNL57MQY53nPSosMY0oRHj6AzCMRplMQpmu+CGUL31d+YFOXGHNMEy8kCgYEA5MoC\nIQuoIKMYjkH/leU14YNkB/oOk5LEB+5opLHAYo0a9wSkE7q5MYmpX7ZdBOsNsRre\ndNDqy9pTLfPj7zNQ8G3DVFSujgQsjnjDp7HGTs+c0eSUAQLtNsk20F5gXtdYRqA1\nuZh0CMXKMSXUWbmYQXgVR3Y9I0E9wwHZ/rFBmnUCgYEAuPevyKdrxYv8/QWnHT3o\neiz9aoMuArt8cc7jZJOgi5bLpXL+k/zE0bQXN0R0g9DvNu67rk+lMNOVQIEDpdv0\nnlfPtXFiHY/GAMqaFAcU1OBNOnYoovIDrRKkflcojU30BYofzaCvMSHB4jf5RqDU\nlW10TgRQbkSUzhCq9036LKECgYEAgJ9AyysufgqzB2b7NV4DCKFBX2qpPzXHl13k\n3pI/wifp/O1TAPR8oOjvm6t+aAFtVR/x6GJ7XdeD49W1UwjafBB5O7PP3m9iTUZ/\nWIuNHUmCtE15F4h5q887TbGBJFCUhEAVdB3NPhFUNoU5+KdqfYPxEpfajzNicXtc\n/t7QLvECgYEAtwKadCXM9VpbN71qYG2pxtPi2qxWnnnBFLhxHJzY3Ra70o9I/Cfg\nI9CCRmLeAMjraWprv14P0tscAvrPrfUw5o9No0t4W6bWY/ibTxM1Op0/kbLxkJbg\n5BaLj9jZ7CzLGlZkxWg1G7lqLT8wz0gznuspueqcz7ZdstW1NDZogCc=\n-----END RSA PRIVATE KEY-----' JWT_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2wo4ugkqLamtl+6AxgCg\n86qAkuVGsh/dQ7SJ1Kb0sLOFrfsBF8SKagQB3JkwB2cTRzD6WyvcvFEGLo4KJAa/\nU9xfMlyCyMCDRCeabCzNsNPKOL5tEW8120X0AERc1g9mRQGwr/uMsBmlwGzutqut\nB/CXKL7sIw0yynJzlltTI+ISGMcbB8aDAOSSuR1hcZCCk3Jaat9M2DdK4/lYmVO/\nIFYdGgYLlItRqHExKmZInJASHIurquxgI72OVq4jXhVCARBlLaQa25oPWcp94+Og\nTS0wYvQl1aE9H9rok9bSd/YES3kLkH3DdjNRPzSRRCX+J8CxUFZw6wFXsVmpCtEM\n3QIDAQAB\n-----END PUBLIC KEY-----' JWT_EXPIRATION=7 #token life 7 days JWT_ISSUER=hesabfun.com JWT_AUDIENCE=https://hesabfun.com
برای تست API از insomnia استفاده میکنیم، برای همه پلتفرمها موجوده، کافیه لاگین کنید تا از اندپوینتها بکآپ بگیره، اینوایرمنت داره و...
خب تا اینجا توکن ایجاد میکنیم و با ارسال دوباره به اندپوینت بعدی چک میکنیم معتبر هست یا نه ولی نیاز داریم فقط با یوزر و پسورد معتبر توکن ایجاد بشه، پس میریم سراغ دیتابیس:
package main import ( "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "upper.io/db.v3" "os" "strconv" "time" ) var claims jws.Claims func loginController(c *gin.Context) { var request struct { Mobile string `json:"mobile" binding:"required,gte=10,lte=12"` Password string `json:"password" binding:"required,gte=0,lte=255"` } if err := c.ShouldBindWith(&request, binding.JSON); err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } var user User err := MySql.Collection("users").Find(db.Cond{ "mobile": request.Mobile, "password": GetMD5Hash(request.Password), }).Where("status IN ('active', 'pending')").One(&user) if err != nil { c.JSON(400, gin.H{"message": "Mobile or password incorrect"}) return } claims = jws.Claims{ "user": struct { Name, Status, Type string }{ Name: user.Name, Status: user.Status, Type: user.Type, }, } token, err := generateToken(user.ID) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } c.JSON(200, gin.H{ "token": token, }) } func parseToken(c *gin.Context) { rsaPublic, _ := crypto.ParseRSAPublicKeyFromPEM([]byte(os.Getenv("JWT_PUBLIC_KEY"))) jwt, err := jws.ParseJWTFromRequest(c.Request) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } if err = jwt.Validate(rsaPublic, crypto.SigningMethodRS256); err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } c.JSON(200, gin.H{ "data": jwt.Claims(), }) } func generateToken(userId uint) (string, error) { exp, _ := strconv.Atoi(os.Getenv("JWT_EXPIRATION")) claims.SetExpiration(time.Now().Add(time.Duration(60 * 60 * 24 * time.Duration(exp) * time.Second))) claims.SetIssuer(os.Getenv("JWT_ISSUER")) claims.SetAudience(os.Getenv("JWT_AUDIENCE")) claims.SetIssuedAt(time.Now()) claims.SetNotBefore(time.Now()) claims.SetSubject(strconv.FormatUint(uint64(userId), 10)) // set user id claims.SetJWTID("123") // set token id rsaPrivate, err := crypto.ParseRSAPrivateKeyFromPEM([]byte(os.Getenv("JWT_PRIVATE_KEY"))) if err != nil { return "", err } jwt := jws.NewJWT(claims, crypto.SigningMethodRS256) token, err := jwt.Serialize(rsaPrivate) if err != nil { return "", err } return string(token), nil }
خیلی خب، کنترولر لاگین یکم تغییر دادیم، اول ولیدیتور تعریف کردیم تا مطمئن باشیم کاربر فیلدهای مربوطه پر کرده، بعد اون کوئری زدیم به دیتابیس اگر کاربری با این پسورد وجود داشته باشه و فعال باشه توکن ایجاد کنه. همچنین تست نوشتیم و یه هندلر برای ساخت MD5 ولی هنوز کارمون با این کنترولر تموم نشده، بعدا کمی تغییرش میدیم و بهترش میکنیم.
قسمت بعد میدلور برای احراز هویت کاربران مینویسیم.
تمام کدها رو گیتهاب هست، میتونید بررسی یا حتی کاملترش کنید :)