Преглед изворни кода

完成用户登录及登录验证

巴拉迪维 пре 3 година
родитељ
комит
fb50c3c836

+ 1 - 1
assets/admin.css

@@ -1,6 +1,6 @@
 
 #login-grid {
-  height: calc(100% - 125px); 
+  height: calc(100% - 115px); 
   margin: 0; 
 }
 

+ 45 - 0
assets/admin.js

@@ -27,6 +27,19 @@ $(document).ready(function() {
               prompt : '密码长度不得少于8位'
             }
           ]
+        },
+        captcha: {
+          identifier  : 'captcha-text',
+          rules: [
+            {
+              type   : 'empty',
+              prompt : '验证码不能为空'
+            },
+            {
+              type   : 'length[6]',
+              prompt : '验证码长度不得少于6位'
+            }
+          ]
         }
       }
     });
@@ -127,4 +140,36 @@ function enable_url(url,state) {
       errorToast($.parseJSON(e.responseText).message)
     } 
   });
+}
+
+function reload_captcha() {
+  $.ajax({
+    type: "POST",
+    url: '/captcha',
+    dataType: 'json',
+    success: function(r) {            
+      $('#captcha-image').html('<img src="/captcha/'+r.result+'.png" />');
+      $('<input>').attr({type: 'hidden', value:r.result ,name: 'captcha-id'}).appendTo('#login-form');
+    },
+    error: function(e) {
+      errorToast($.parseJSON(e.responseText).message)
+    }
+  });
+}
+
+function sign_out_config() {
+  $('body').modal('confirm','温馨提示','确认退出 ohUrlShortener 管理后端吗?', function(choice){
+    if (choice) {
+      $.ajax({
+        type:"POST",
+        url: "/admin/logout",
+        success: function() {
+          successToast('操作成功,再见!')
+        },
+        error: function(e) {
+          errorToast($.parseJSON(e.responseText).message)
+        }
+      });
+    }
+  });
 }

+ 65 - 2
controller/admin_controller.go

@@ -17,6 +17,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/dchest/captcha"
 	"github.com/gin-gonic/gin"
 )
 
@@ -32,11 +33,73 @@ func LoginPage(c *gin.Context) {
 }
 
 func DoLogin(c *gin.Context) {
-	//TODO: Login logic
+	account := c.PostForm("account")
+	password := c.PostForm("password")
+	captchaText := c.PostForm("captcha-text")
+	captchaId := c.PostForm("captcha-id")
+
+	if utils.EemptyString(account) || utils.EemptyString(password) || len(account) < 5 || len(password) < 8 {
+		c.HTML(http.StatusOK, "login.html", gin.H{
+			"title": "错误 - ohUrlShortener",
+			"error": "用户名或密码格式错误!",
+		})
+		return
+	}
+
+	if utils.EemptyString(captchaText) || utils.EemptyString(captchaId) || len(captchaText) < 6 {
+		c.HTML(http.StatusOK, "login.html", gin.H{
+			"title": "错误 - ohUrlShortener",
+			"error": "验证码格式错误!",
+		})
+		return
+	}
+
+	//验证码有效性验证
+	if !captcha.VerifyString(captchaId, captchaText) {
+		c.HTML(http.StatusOK, "login.html", gin.H{
+			"title": "错误 - ohUrlShortener",
+			"error": "验证码错误,请刷新页面再重新尝试!",
+		})
+		return
+	}
+
+	//用户名密码有效性验证
+	loginUser, err := service.Login(account, password)
+	if err != nil || loginUser.IsEmpty() {
+		c.HTML(http.StatusOK, "login.html", gin.H{
+			"title": "错误 - ohUrlShortener",
+			"error": err.Error(),
+		})
+		return
+	}
+
+	//Write Cookie to browser
+	cValue, err := AdminCookieValue(loginUser)
+	if err != nil {
+		c.HTML(http.StatusOK, "login.html", gin.H{
+			"title": "错误 - ohUrlShortener",
+			"error": "内部错误,请联系管理员",
+		})
+		return
+	}
+	c.SetCookie("ohUrlShortenerAdmin", loginUser.Account, 3600, "/", "", true, true)
+	c.SetCookie("ohUrlShortenerCookie", cValue, 3600, "/", "", true, true)
+	c.Redirect(http.StatusFound, "/admin/dashboard")
 }
 
 func DoLogout(c *gin.Context) {
-	//TODO: Login logic
+	c.SetCookie("ohUrlShortenerAdmin", "", -1, "/", "", true, true)
+	c.SetCookie("ohUrlShortenerCookie", "", -1, "/", "", true, true)
+	c.Redirect(http.StatusFound, "/login")
+}
+
+func ServeCaptchaImage(c *gin.Context) {
+	captcha.Server(200, 45).ServeHTTP(c.Writer, c.Request)
+}
+
+func RequestCaptchaImage(c *gin.Context) {
+	imageId := captcha.New()
+	c.JSON(http.StatusOK, core.ResultJsonSuccessWithData(imageId))
 }
 
 func ChangeState(c *gin.Context) {

+ 49 - 2
controller/handlers.go

@@ -10,16 +10,63 @@ package controller
 
 import (
 	"fmt"
+	"log"
+	"net/http"
+	"ohurlshortener/core"
+	"ohurlshortener/service"
+	"ohurlshortener/utils"
+	"strconv"
 	"strings"
 
 	"github.com/gin-gonic/gin"
 )
 
+func AdminCookieValue(user core.User) (string, error) {
+	var result string
+	data, err := utils.Sha256Of(user.Account + "a=" + user.Password + "=e" + strconv.Itoa(user.ID))
+	if err != nil {
+		log.Println(err)
+		return result, err
+	}
+	return utils.Base58Encode(data), nil
+}
+
 func AdminAuthHandler() gin.HandlerFunc {
 	return func(c *gin.Context) {
-		c.Set("current_url", c.Request.URL.Path)
+		user, err := c.Cookie("ohUrlShortenerAdmin")
+		if err != nil {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		cookie, err := c.Cookie("ohUrlShortenerCookie")
+		if err != nil {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		if len(user) <= 0 || len(cookie) <= 0 {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		found, err := service.GetUserByAccountFromRedis(user)
+		if err != nil {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		if found.IsEmpty() {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		cValue, err := AdminCookieValue(found)
+		if err != nil {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
+		if !strings.EqualFold(cValue, cookie) {
+			c.Redirect(http.StatusFound, "/login")
+		}
+
 		c.Next()
-	}
+	} //end of func
 }
 
 func WebLogFormatHandler(server string) gin.HandlerFunc {

+ 13 - 0
core/user.go

@@ -0,0 +1,13 @@
+package core
+
+import "reflect"
+
+type User struct {
+	ID       int    `db:"id"`
+	Account  string `db:"account"`
+	Password string `db:"password"`
+}
+
+func (user User) IsEmpty() bool {
+	return reflect.DeepEqual(user, User{})
+}

+ 1 - 0
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/sprig v2.22.0+incompatible
 	github.com/btcsuite/btcutil v1.0.2
+	github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f // indirect
 	github.com/gin-gonic/gin v1.7.7
 	github.com/go-redis/redis/v8 v8.11.4
 	github.com/google/uuid v1.3.0 // indirect

+ 2 - 0
go.sum

@@ -23,6 +23,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
+github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=

+ 6 - 1
main.go

@@ -53,6 +53,9 @@ func init() {
 
 	_, err = service.ReloadUrls()
 	utils.PrintOnError("Realod urls failed.", err)
+
+	err = service.ReloadUsers()
+	utils.PrintOnError("Realod users failed.", err)
 }
 
 func main() {
@@ -160,12 +163,14 @@ func initializeRoute02() (http.Handler, error) {
 	})
 	router.GET("/login", controller.LoginPage)
 	router.POST("/login", controller.DoLogin)
-	router.POST("/logout", controller.DoLogout)
+	router.GET("/captcha/:imageId", controller.ServeCaptchaImage)
+	router.POST("/captcha", controller.RequestCaptchaImage)
 
 	admin := router.Group("/admin", controller.AdminAuthHandler())
 	admin.GET("/", func(ctx *gin.Context) {
 		ctx.Redirect(http.StatusTemporaryRedirect, "/admin/dashboard")
 	})
+	admin.POST("/logout", controller.DoLogout)
 	admin.GET("/dashboard", controller.DashbaordPage)
 	admin.GET("/urls", controller.UrlsPage)
 	admin.GET("/stats", controller.StatsPage)

+ 65 - 0
service/users_s.go

@@ -0,0 +1,65 @@
+package service
+
+import (
+	"encoding/json"
+	"ohurlshortener/core"
+	"ohurlshortener/storage"
+	"ohurlshortener/utils"
+	"strings"
+)
+
+const ADMIN_USER_PREFIX = "ohUrlShortenerAdmin#"
+const ADMIN_COOKIE_PREFIX = "ohUrlShortenerCookie#"
+
+func Login(account string, pasword string) (core.User, error) {
+
+	var found core.User
+	found, err := GetUserByAccountFromRedis(account)
+	if err != nil {
+		return found, utils.RaiseError("内部错误,请联系管理员")
+	}
+
+	if found.IsEmpty() {
+		return found, utils.RaiseError("用户名或密码错误")
+	}
+
+	res, err := storage.PasswordBase58Hash(pasword)
+	if err != nil {
+		return found, utils.RaiseError("内部错误,请联系管理员")
+	}
+
+	if !strings.EqualFold(found.Password, res) {
+		return found, utils.RaiseError("用户名或密码错误")
+	}
+
+	return found, nil
+}
+
+func ReloadUsers() error {
+	users, err := storage.FindAllUsers()
+	if err != nil {
+		return err
+	}
+
+	for _, user := range users {
+		jsonUser, _ := json.Marshal(user)
+		er := storage.RedisSet4Ever(ADMIN_USER_PREFIX+user.Account, jsonUser)
+		if er != nil {
+			return er
+		}
+	}
+
+	return nil
+}
+
+func GetUserByAccountFromRedis(account string) (core.User, error) {
+	var found core.User
+	foundUserStr, err := storage.RedisGetString(ADMIN_USER_PREFIX + account)
+	if err != nil {
+		return found, err
+	}
+
+	json.Unmarshal([]byte(foundUserStr), &found)
+
+	return found, nil
+}

+ 43 - 0
storage/users_storage.go

@@ -0,0 +1,43 @@
+package storage
+
+import (
+	"ohurlshortener/core"
+	"ohurlshortener/utils"
+	"strings"
+
+	"github.com/btcsuite/btcutil/base58"
+)
+
+func FindAllUsers() ([]core.User, error) {
+	var found []core.User
+	query := `SELECT * FROM public.users u`
+	return found, DbSelect(query, &found)
+}
+
+func NewUser(account string, password string) error {
+	query := `INSERT INTO public.users (account, "password") VALUES(:account,:password)`
+	data, err := PasswordBase58Hash(password)
+	if err != nil {
+		return err
+	}
+	return DbNamedExec(query, core.User{Account: account, Password: data})
+}
+
+func UpdateUser(user core.User) error {
+	query := `UPDATE public.users SET account = :account , "password" = :password WHERE id = :id`
+	return DbNamedExec(query, user)
+}
+
+func FindUserByAccount(account string) (core.User, error) {
+	var user core.User
+	query := `SELECT * FROM public.users u WHERE lower(u.account) = $1`
+	return user, DbGet(query, &user, strings.ToLower(account))
+}
+
+func PasswordBase58Hash(password string) (string, error) {
+	data, err := utils.Sha256Of(password)
+	if err != nil {
+		return "", err
+	}
+	return base58.Encode(data), nil
+}

+ 24 - 0
storage/users_storage_test.go

@@ -0,0 +1,24 @@
+package storage
+
+import (
+	"ohurlshortener/utils"
+	"testing"
+)
+
+func TestNewUser(t *testing.T) {
+	init4Test(t)
+	NewUser("ohUrlShortener", "-2aDzm=0(ln_9^1")
+}
+
+func init4Test(t *testing.T) {
+	_, err := utils.InitConfig("../config.ini")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, err = InitDatabaseService()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+}

+ 12 - 0
structure.sql

@@ -28,6 +28,18 @@ CREATE INDEX access_logs_access_time_idx ON public.access_logs (access_time);
 CREATE INDEX access_logs_ip_idx ON public.access_logs (ip);
 CREATE INDEX access_logs_ua_idx ON public.access_logs (user_agent);
 
+CREATE TABLE public.users (
+  id serial4 NOT NULL,
+	account varchar(200) NOT NULL,
+	password text NOT NULL,			
+	CONSTRAINT users_pk PRIMARY KEY (id),
+	CONSTRAINT users_account_un UNIQUE (account)
+);
+
+-- account: ohUrlShortener password: -2aDzm=0(ln_9^1
+INSERT INTO public.users (account, "password") VALUES('ohUrlShortener', 'EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t');
+
+
 CREATE VIEW public.url_ip_count_stats AS
 SELECT
 	u.short_url AS short_url,	

+ 2 - 1
templates/admin/dashboard.html

@@ -54,7 +54,8 @@
         </div>
       </div>
     </div><!--end of column-->
-  </div><!--end of grid-->  
+  </div><!--end of grid-->
+  
   <div class="ui grid stackable padded">
     <div class="column">
       <table class="ui celled striped table">

+ 11 - 0
templates/admin/login.html

@@ -28,6 +28,17 @@
             <input type="password" name="password" placeholder="Password">
           </div>
         </div>
+        <div class="field">
+          <div class="two fields">
+            <div class="field">
+              <div class="ui left icon input">
+                <i class="image icon"></i>
+                <input type="text" name="captcha-text" placeholder="验证码">
+              </div>              
+            </div>
+            <div id="captcha-image" class="field"><a class="ui input" href="javascript:reload_captcha()">点击获取验证码</a></div>
+          </div>  
+        </div>     
         <div class="ui fluid large black submit button">Login</div>
       </div>  
       <div class="ui error message"></div>

+ 2 - 2
templates/admin/menu.html

@@ -9,7 +9,7 @@
       <a class="{{if eq .current_url "/admin/access_logs"}}active {{end}}item" target="_self"  href="/admin/access_logs">访问日志</a> 
     </div>    
   </div>   
-  <a class="item" href="/admin/logout" target="_self">安全退出</a>
+  <a class="item" href="javascript:sign_out_config()" target="_self">安全退出</a>
 </div><!--end of sidebar-menu-->
 {{end -}}
 
@@ -31,6 +31,6 @@
       <a class="{{if eq .current_url "/admin/access_logs"}}active {{end}}item" target="_self"  href="/admin/access_logs">访问日志</a> 
     </div>    
   </div>      
-  <a class="item" target="_self" href="/admin/dashboard">安全退出</a>
+  <a class="item" target="_self" href="javascript:sign_out_config()">安全退出</a>
 </div><!--end of left-menu-->
 {{end -}}