You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/08/21 10:45:29 UTC

[apisix-dashboard] branch master updated: feat(authentication): create authentication module (#330)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 98d488b  feat(authentication): create authentication module (#330)
98d488b is described below

commit 98d488bc45c8e293911588d51caf73c89e678e2f
Author: bzp2010 <bz...@gmail.com>
AuthorDate: Fri Aug 21 18:45:18 2020 +0800

    feat(authentication): create authentication module (#330)
    
    * feat(authentication): create module typing definition
    
    * feat(authentication): create Login page
    
    * feat(authentication): update typing definition
    
    * feat(authentication): add centent to Login page
    
    * feat(authentication): update typing definition
    
    * feat(authentication): update Login page to add Password and Test method
    
    * feat(authentication): update typing definition to add check and submit function
    
    * feat(authentication): move Test login method to Example
    
    * feat(authentication): add check and submit function
    
    * feat(authentication): add submit function in Login page
    
    * feat(authentication): add test to Password login method
    
    * feat(authentication): change example LoginMethod text
    
    * feat(authentication): add i18n content
    
    * feat(authentication): redirect to index when login success
    
    * feat(i18n): update i18n file import
    remove import i18n file of user module manually and try auto import by umi.js
    
    * feat(authentication): create authentication configure items
    
    * fix(authentication): fix logging filter
    write back request body for read by PostForm function
    
    * feat(authentication): create authentication controller
    
    * feat(authentication): update dependencies
    
    * fix(authentication): fix logging filter
    
    * feat(authentication): change to session for authentication
    
    * feat(authentication): create authentication filter
    use authentication filter to check every request
    
    * feat(authentication): create unit test case
    
    * fix(authentication): change HTTP code when authentication fail request
    
    * feat(authentication): add jwt dependency
    
    * feat(authentication): create session configures
    
    * feat(authentication): change cookie-based session to jwt
    
    * feat(authentication): change cors Access-Control-Allow-Headers header
    
    * feat(authentication): change login page path and error handler
    
    * feat(authentication): create request interceptor to add Authorization header
    
    * feat(authentication): connect to backend login API and i18n
    
    * feat(authentication): create logout page
    
    * feat(authentication): add redirect query to back previous page
    
    * feat(authentication): update LoginMethod definition for logout
    
    * feat(authentication): add logout button
    
    * feat(authentication): improve login page
    
    * fix: clean codes
    
    * fix(authentication): fix unit test crash
    
    * feat(authentication): remove API url setting
    
    * feat(authentication): improve session check
    
    * feat(authentication): redirect to login page when not exist token
    
    * fix: clean codes and add ASF header
---
 api/conf/conf.go                                   |  35 ++++++
 api/conf/conf.json                                 |  16 +++
 api/errno/error.go                                 |   4 +
 api/filter/authentication.go                       |  66 ++++++++++
 api/filter/cors.go                                 |   2 +-
 api/filter/logging.go                              |   5 +
 api/go.mod                                         |   5 +-
 api/go.sum                                         |  40 ++++++
 api/route/authentication.go                        |  68 +++++++++++
 api/route/{base_test.go => authentication_test.go} |  40 ++++--
 api/route/base.go                                  |  16 ++-
 api/route/base_test.go                             |   7 +-
 api/route/consumer_test.go                         |   1 +
 config/routes.ts                                   |  10 ++
 src/app.tsx                                        |  20 ++-
 src/components/RightContent/AvatarDropdown.tsx     |  21 +++-
 src/helpers.tsx                                    |  19 ++-
 src/locales/en-US/component.ts                     |   9 --
 src/locales/zh-CN/component.ts                     |   9 --
 src/pages/Setting/Setting.tsx                      |   4 +-
 src/pages/User/Login.less                          | 133 ++++++++++++++++++++
 src/pages/User/Login.tsx                           | 123 +++++++++++++++++++
 .../base_test.go => src/pages/User/Logout.tsx      |  40 +++---
 .../pages/User/components/LoginMethodExample.tsx   |  45 ++++---
 src/pages/User/components/LoginMethodPassword.tsx  | 135 +++++++++++++++++++++
 src/pages/User/index.ts                            |   2 +
 src/pages/User/locales/en-US.ts                    |  14 +++
 src/pages/User/locales/zh-CN.ts                    |  13 ++
 src/pages/User/typing.d.ts                         |  25 ++++
 29 files changed, 841 insertions(+), 86 deletions(-)

diff --git a/api/conf/conf.go b/api/conf/conf.go
index f4d7b77..1ed7ecb 100644
--- a/api/conf/conf.go
+++ b/api/conf/conf.go
@@ -45,6 +45,7 @@ func init() {
 	setEnvironment()
 	initMysql()
 	initApisix()
+	initAuthentication()
 }
 
 func setEnvironment() {
@@ -74,7 +75,22 @@ type mysqlConfig struct {
 	MaxLifeTime  int
 }
 
+type user struct {
+	Username string
+	Password string
+}
+
+type authenticationConfig struct {
+  Session struct {
+    Secret     string
+    ExpireTime uint64
+  }
+}
+
+var UserList = make(map[string]user, 1)
+
 var MysqlConfig mysqlConfig
+var AuthenticationConfig authenticationConfig
 
 func initMysql() {
 	filePath := configurationPath()
@@ -103,3 +119,22 @@ func initApisix() {
 		ApiKey = apisixConf.Get("api_key").String()
 	}
 }
+
+func initAuthentication() {
+	filePath := configurationPath()
+	if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+		panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+	} else {
+		configuration := gjson.ParseBytes(configurationContent)
+		userList := configuration.Get("authentication.user").Array()
+
+		// create user list
+		for _, item := range userList{
+			username := item.Map()["username"].String()
+			password := item.Map()["password"].String()
+			UserList[item.Map()["username"].String()] = user{Username: username, Password: password}
+		}
+    AuthenticationConfig.Session.Secret =  configuration.Get("authentication.session.secret").String()
+    AuthenticationConfig.Session.ExpireTime =  configuration.Get("authentication.session.expireTime").Uint()
+	}
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
index 31731c3..d160087 100644
--- a/api/conf/conf.json
+++ b/api/conf/conf.json
@@ -15,5 +15,21 @@
       "base_url": "http://127.0.0.1:9080/apisix/admin",
       "api_key": "edd1c9f034335f136f87ad84b625c8f1"
     }
+  },
+  "authentication": {
+    "session": {
+      "secret": "secret",
+      "expireTime": 3600
+    },
+    "user": [
+      {
+        "username": "admin",
+        "password": "admin"
+      },
+      {
+        "username": "user",
+        "password": "user"
+      }
+    ]
   }
 }
diff --git a/api/errno/error.go b/api/errno/error.go
index 08b5adb..93180d8 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -42,6 +42,7 @@ var (
 	InvalidParamDetail = Message{"010010", "Invalid request parameter: %s", 400}
 	AdminApiSaveError  = Message{"010011", "Data save failed", 500}
 	SchemaCheckFailed  = Message{"010012", "%s", 400}
+	ForbiddenError     = Message{"010013", "Request Unauthorized", 401}
 
 	//BB 01 configuration
 	ConfEnvError      = Message{"010101", "Environment variable not found: %s", 500}
@@ -84,6 +85,9 @@ var (
 	ApisixConsumerUpdateError = Message{"010703", "APISIX Consumer update failed", 500}
 	ApisixConsumerDeleteError = Message{"010704", "APISIX Consumer delete failed", 500}
 	DuplicateUserName         = Message{"010705", "Duplicate consumer username", 400}
+
+	// 99 authentication
+	AuthenticationUserError = Message{"019901", "username or password error", 401}
 )
 
 type ManagerError struct {
diff --git a/api/filter/authentication.go b/api/filter/authentication.go
new file mode 100644
index 0000000..11c1561
--- /dev/null
+++ b/api/filter/authentication.go
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	"github.com/dgrijalva/jwt-go"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"strings"
+)
+
+func Authentication() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		if c.Request.URL.Path != "/user/login" && strings.HasPrefix(c.Request.URL.Path,"/apisix") {
+			tokenStr := c.GetHeader("Authorization")
+
+			// verify token
+			token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
+				return []byte(conf.AuthenticationConfig.Session.Secret), nil
+			})
+
+			if err != nil {
+				c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+				return
+			}
+
+			claims, ok := token.Claims.(*jwt.StandardClaims)
+			if !ok {
+				c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+				return
+			}
+
+			if err := token.Claims.Valid(); err != nil {
+				c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+				return
+			}
+
+			if claims.Subject == "" {
+				c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+				return
+			}
+
+			if _, ok := conf.UserList[claims.Subject]; !ok {
+        c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+        return
+      }
+		}
+		c.Next()
+	}
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
index f2c7ac1..b33c62b 100644
--- a/api/filter/cors.go
+++ b/api/filter/cors.go
@@ -22,7 +22,7 @@ func CORS() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
 		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
-		c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
 		c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
 		if c.Request.Method == "OPTIONS" {
 			c.AbortWithStatus(204)
diff --git a/api/filter/logging.go b/api/filter/logging.go
index b1a40f6..6318d52 100644
--- a/api/filter/logging.go
+++ b/api/filter/logging.go
@@ -18,6 +18,7 @@ package filter
 
 import (
 	"bytes"
+	"io/ioutil"
 	"time"
 
 	"github.com/apisix/manager-api/errno"
@@ -33,11 +34,15 @@ func RequestLogHandler() gin.HandlerFunc {
 			val = c.Request.URL.Query()
 		} else {
 			val, _ = c.GetRawData()
+
+			// set RequestBody back
+			c.Request.Body = ioutil.NopCloser(bytes.NewReader(val.([]byte)))
 		}
 		c.Set("requestBody", val)
 		uuid, _ := c.Get("X-Request-Id")
 
 		param, _ := c.Get("requestBody")
+
 		switch param.(type) {
 		case []byte:
 			param = string(param.([]byte))
diff --git a/api/go.mod b/api/go.mod
index ab07faa..0ebf639 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -4,12 +4,15 @@ go 1.13
 
 require (
 	github.com/api7/apitest v1.4.9
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/gin-contrib/pprof v1.3.0
+	github.com/gin-contrib/sessions v0.0.3
 	github.com/gin-gonic/gin v1.6.3
-	github.com/go-sql-driver/mysql v1.5.0
+	github.com/go-sql-driver/mysql v1.5.0 // indirect
 	github.com/jinzhu/gorm v1.9.12
 	github.com/satori/go.uuid v1.2.0
 	github.com/sirupsen/logrus v1.6.0
+	github.com/steinfletcher/apitest v1.4.9 // indirect
 	github.com/stretchr/testify v1.6.1
 	github.com/tidwall/gjson v1.6.0
 	gopkg.in/resty.v1 v1.12.0
diff --git a/api/go.sum b/api/go.sum
index 1daa324..b82d22e 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,20 +1,34 @@
 github.com/api7/apitest v1.4.9 h1:FYTUQJ1hgeB9UvMFif1jjbfiA+XqHPEBfsjhDskytA8=
 github.com/api7/apitest v1.4.9/go.mod h1:YZruZ+jDMFL6rNgMWiuhwCTugNN0mJkLCYCHG3ICYlE=
+github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
+github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
+github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
 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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
 github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
 github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
+github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
 github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
 github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
 github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
@@ -22,36 +36,57 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
+github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
+github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
 github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
 github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
 github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/steinfletcher/apitest v1.4.9 h1:8X7G+1m+GngIo5LFfDM0CxLSG9jcJn9LLeDH/Ov144M=
+github.com/steinfletcher/apitest v1.4.9/go.mod h1:0MT98QwexQVvf5pIn3fqiC/+8Nyd7A4RShxuSjnpOcE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -71,6 +106,7 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -79,13 +115,17 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/api/route/authentication.go b/api/route/authentication.go
new file mode 100644
index 0000000..c19fa6a
--- /dev/null
+++ b/api/route/authentication.go
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+	"github.com/apisix/manager-api/conf"
+	"github.com/apisix/manager-api/errno"
+	jwt "github.com/dgrijalva/jwt-go"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"time"
+)
+
+type UserSession struct {
+	Token string `json:"token"`
+}
+
+func AppendAuthentication(r *gin.Engine) *gin.Engine {
+	r.POST("/user/login", userLogin)
+	return r
+}
+
+func userLogin(c *gin.Context) {
+	username := c.PostForm("username")
+	password := c.PostForm("password")
+
+	if username == "" {
+		c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "username is needed").Response())
+		return
+	}
+	if password == "" {
+		c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "password is needed").Response())
+		return
+	}
+
+	user := conf.UserList[username]
+	if username != user.Username || password != user.Password {
+		c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.AuthenticationUserError).Response())
+	} else {
+		// create JWT for session
+		claims := jwt.StandardClaims{
+			Subject: username,
+			IssuedAt: time.Now().Unix(),
+			ExpiresAt: time.Now().Add(time.Second * time.Duration(conf.AuthenticationConfig.Session.ExpireTime)).Unix(),
+		}
+		token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+		signedToken, _ := token.SignedString([]byte(conf.AuthenticationConfig.Session.Secret))
+
+		// output token
+		c.AbortWithStatusJSON(http.StatusOK, errno.FromMessage(errno.SystemSuccess).ItemResponse(&UserSession {
+			Token: signedToken,
+		}))
+	}
+}
diff --git a/api/route/base_test.go b/api/route/authentication_test.go
similarity index 52%
copy from api/route/base_test.go
copy to api/route/authentication_test.go
index 3e3a096..0cc0214 100644
--- a/api/route/base_test.go
+++ b/api/route/authentication_test.go
@@ -17,22 +17,36 @@
 package route
 
 import (
-	"github.com/api7/apitest"
-
-	"github.com/apisix/manager-api/conf"
+  "bytes"
+  "net/http"
+  "strings"
+  "testing"
 )
 
-var handler *apitest.APITest
-
-var (
-	uriPrefix = "/apisix/admin"
-)
+var token string
 
-func init() {
-	//init mysql connect
-	conf.InitializeMysql()
+func TestUserLogin(t *testing.T) {
+  // password error
+  handler.
+    Post("/user/login").
+    Header("Content-Type", "application/x-www-form-urlencoded").
+    Body("username=admin&password=admin1").
+    Expect(t).
+    Status(http.StatusUnauthorized).
+    End()
 
-	r := SetUpRouter()
+  // login success
+  sessionResponse := handler.
+    Post("/user/login").
+    Header("Content-Type", "application/x-www-form-urlencoded").
+    Body("username=admin&password=admin").
+    Expect(t).
+    Status(http.StatusOK).
+    End().Response.Body
 
-	handler = apitest.New().Handler(r)
+  buf := new(bytes.Buffer)
+  buf.ReadFrom(sessionResponse)
+  data := buf.String()
+  tokenArr := strings.Split(data, "\"token\":\"")
+  token = strings.Split(tokenArr[1], "\"}")[0]
 }
diff --git a/api/route/base.go b/api/route/base.go
index 51dd90b..1b48d4a 100644
--- a/api/route/base.go
+++ b/api/route/base.go
@@ -17,11 +17,13 @@
 package route
 
 import (
-	"github.com/gin-contrib/pprof"
-	"github.com/gin-gonic/gin"
+  "github.com/gin-contrib/pprof"
+  "github.com/gin-contrib/sessions"
+  "github.com/gin-contrib/sessions/cookie"
+  "github.com/gin-gonic/gin"
 
-	"github.com/apisix/manager-api/conf"
-	"github.com/apisix/manager-api/filter"
+  "github.com/apisix/manager-api/conf"
+  "github.com/apisix/manager-api/filter"
 )
 
 func SetUpRouter() *gin.Engine {
@@ -31,10 +33,12 @@ func SetUpRouter() *gin.Engine {
 		gin.SetMode(gin.ReleaseMode)
 	}
 	r := gin.New()
-
-	r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+	store := cookie.NewStore([]byte("secret"))
+	r.Use(sessions.Sessions("session", store))
+	r.Use(filter.CORS(), filter.Authentication(),filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
 
 	AppendHealthCheck(r)
+	AppendAuthentication(r)
 	AppendRoute(r)
 	AppendSsl(r)
 	AppendPlugin(r)
diff --git a/api/route/base_test.go b/api/route/base_test.go
index 3e3a096..007af13 100644
--- a/api/route/base_test.go
+++ b/api/route/base_test.go
@@ -18,7 +18,6 @@ package route
 
 import (
 	"github.com/api7/apitest"
-
 	"github.com/apisix/manager-api/conf"
 )
 
@@ -34,5 +33,9 @@ func init() {
 
 	r := SetUpRouter()
 
-	handler = apitest.New().Handler(r)
+	handler = apitest.
+		New().
+		Handler(r)
 }
+
+
diff --git a/api/route/consumer_test.go b/api/route/consumer_test.go
index 60066bb..b32c38f 100644
--- a/api/route/consumer_test.go
+++ b/api/route/consumer_test.go
@@ -27,6 +27,7 @@ func TestConsumer(t *testing.T) {
 	// create ok
 	handler.
 		Post(uriPrefix + "/consumers").
+    Header("Authorization", token).
 		JSON(`{
 			"username": "e2e_test_consumer1",
 			"plugins": {
diff --git a/config/routes.ts b/config/routes.ts
index 9851a95..6648413 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -76,6 +76,16 @@ const routes = [
     component: './Setting',
   },
   {
+    path: '/user/login',
+    component: './User/Login',
+    layout: false,
+  },
+  {
+    path: '/user/logout',
+    component: './User/Logout',
+    layout: false,
+  },
+  {
     component: './404',
   },
 ];
diff --git a/src/app.tsx b/src/app.tsx
index d309fac..b437cc9 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import React from 'react';
-import { RequestConfig } from 'umi';
+import { RequestConfig, history } from 'umi';
 import {
   BasicLayoutProps,
   Settings as LayoutSettings,
@@ -32,6 +32,11 @@ export async function getInitialState(): Promise<{
   currentUser?: API.CurrentUser;
   settings?: LayoutSettings;
 }> {
+  const token = localStorage.getItem('token');
+  if (!token) {
+    history.replace(`/user/login?redirect=${encodeURIComponent(window.location.pathname)}`);
+  }
+
   const currentUser = await queryCurrent();
   return {
     currentUser,
@@ -59,4 +64,17 @@ export const request: RequestConfig = {
   prefix: '/apisix/admin',
   errorHandler,
   credentials: 'same-origin',
+  requestInterceptors: [
+    (url, options) => {
+      const newOptions = options;
+      newOptions.headers = {
+        ...options.headers,
+        Authorization: localStorage.getItem('token') || '',
+      };
+      return {
+        url,
+        options: { ...newOptions, interceptors: true },
+      };
+    },
+  ],
 };
diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx
index b3fbf9d..88c8c39 100644
--- a/src/components/RightContent/AvatarDropdown.tsx
+++ b/src/components/RightContent/AvatarDropdown.tsx
@@ -22,9 +22,8 @@
 * SOFTWARE.
 */
 import React, { useCallback } from 'react';
-import { SettingOutlined, UserOutlined, SettingFilled } from '@ant-design/icons';
+import { SettingOutlined, UserOutlined, SettingFilled, LogoutOutlined } from '@ant-design/icons';
 import { Avatar, Menu, Spin } from 'antd';
-import { ClickParam } from 'antd/es/menu';
 import { history, useModel } from 'umi';
 
 import { stringify } from 'querystring';
@@ -50,13 +49,24 @@ const settings = async () => {
 const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
   const { initialState, setInitialState } = useModel('@@initialState');
 
-  const onMenuClick = useCallback((event: ClickParam) => {
+  const onMenuClick = useCallback((event) => {
     const { key } = event;
     if (key === 'settings') {
       setInitialState({ ...initialState, currentUser: undefined });
       settings();
       return;
     }
+
+    if (key === 'logout') {
+      setInitialState({ ...initialState, currentUser: undefined });
+      history.replace({
+        pathname: '/user/logout',
+        search: stringify({
+          redirect: window.location.pathname,
+        }),
+      });
+      return;
+    }
     history.push(`/account/${key}`);
   }, []);
 
@@ -102,6 +112,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
         <SettingFilled />
         修改设置
       </Menu.Item>
+      <Menu.Divider />
+      <Menu.Item key="logout">
+        <LogoutOutlined />
+        退出
+      </Menu.Item>
     </Menu>
   );
   return (
diff --git a/src/helpers.tsx b/src/helpers.tsx
index f644c22..39e97da 100644
--- a/src/helpers.tsx
+++ b/src/helpers.tsx
@@ -17,6 +17,7 @@
 import React from 'react';
 import { notification } from 'antd';
 import { MenuDataItem } from '@ant-design/pro-layout';
+import { history } from 'umi';
 
 import { codeMessage } from './constants';
 import IconFont from './iconfont';
@@ -56,7 +57,7 @@ export const getMenuData = (): MenuDataItem[] => {
   ];
 };
 
-export const isLoginPage = () => window.location.pathname.indexOf('/login') !== -1;
+export const isLoginPage = () => window.location.pathname.indexOf('/user/login') !== -1;
 
 /**
  * 异常处理程序
@@ -65,9 +66,10 @@ export const errorHandler = (error: { response: Response; data: any }): Promise<
   const { response } = error;
   if (response && response.status) {
     if ([401].includes(response.status) && !isLoginPage()) {
-      localStorage.clear();
-      window.location.href = '/login';
+      history.replace(`/user/logout?redirect=${encodeURIComponent(window.location.pathname)}`);
+      return Promise.reject(response);
     }
+    if ([401].includes(response.status) && isLoginPage()) return Promise.reject(response);
 
     const errorText =
       error.data.msg || error.data.message || error.data.error_msg || codeMessage[response.status];
@@ -84,3 +86,14 @@ export const errorHandler = (error: { response: Response; data: any }): Promise<
   }
   return Promise.reject(response);
 };
+
+export const getUrlQuery: (key: string) => string | false = (key: string) => {
+  const query = window.location.search.substring(1);
+  const vars = query.split('&');
+
+  for (let i = 0; i < vars.length; i += 1) {
+    const pair = vars[i].split('=');
+    if (pair[0] === key) return pair[1];
+  }
+  return false;
+};
diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts
index fc5e8da..85b2a4b 100644
--- a/src/locales/en-US/component.ts
+++ b/src/locales/en-US/component.ts
@@ -32,15 +32,6 @@ export default {
   'component.global.loading': 'Loading',
   'component.status.success': 'Successfully',
   'component.status.fail': 'Failed',
-  // User component
-  'component.user.loginByPassword': 'Username & Password',
-  'component.user.login': 'Login',
-  'component.user.username': 'Username',
-  'component.user.password': 'Password',
-  'component.user.rememberMe': 'Remember Me',
-  'component.user.inputUsername': 'Please input username!',
-  'component.user.inputPassword': 'Please input password!',
-  'component.user.wrongUsernameOrPassword': 'Wrong account or password!',
   // SSL Module
   'component.ssl.removeSSLItemModalContent': 'You are going to remove this item!',
   'component.ssl.removeSSLItemModalTitle': 'SSL Remove Alert',
diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts
index f2ca2d8..0e4f487 100644
--- a/src/locales/zh-CN/component.ts
+++ b/src/locales/zh-CN/component.ts
@@ -32,15 +32,6 @@ export default {
   'component.global.loading': '加载中',
   'component.status.success': '成功',
   'component.status.fail': '失败',
-  // User component
-  'component.user.loginByPassword': '账号密码登录',
-  'component.user.login': '登录',
-  'component.user.username': '账号',
-  'component.user.password': '密码',
-  'component.user.rememberMe': '自动登录',
-  'component.user.inputUsername': '请输入账号!',
-  'component.user.inputPassword': '请输入密码!',
-  'component.user.wrongUsernameOrPassword': '账号或密码错误!',
   // SSL Module
   'component.ssl.removeSSLItemModalContent': '确定要删除该项吗?',
   'component.ssl.removeSSLItemModalTitle': '删除 SSL',
diff --git a/src/pages/Setting/Setting.tsx b/src/pages/Setting/Setting.tsx
index 6b95859..65c1dab 100644
--- a/src/pages/Setting/Setting.tsx
+++ b/src/pages/Setting/Setting.tsx
@@ -19,6 +19,7 @@ import { PageContainer } from '@ant-design/pro-layout';
 import { Card, Form, Input, Row, Col, notification } from 'antd';
 import { useIntl } from 'umi';
 
+import { getUrlQuery } from '@/helpers';
 import ActionBar from '@/components/ActionBar';
 import { getGrafanaURL } from '@/pages/Metrics/service';
 
@@ -60,7 +61,8 @@ const Setting: React.FC = () => {
         }),
       });
       setTimeout(() => {
-        window.location.reload();
+        const redirect = getUrlQuery('redirect');
+        window.location.href = redirect ? decodeURIComponent(redirect) : '/';
       }, 500);
     });
   };
diff --git a/src/pages/User/Login.less b/src/pages/User/Login.less
new file mode 100644
index 0000000..8635c96
--- /dev/null
+++ b/src/pages/User/Login.less
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '~antd/es/style/themes/default.less';
+
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  overflow: auto;
+  background: @layout-body-background;
+}
+
+.lang {
+  width: 100%;
+  height: 42px;
+  line-height: 42px;
+  text-align: right;
+}
+
+.settings {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12px;
+  font-size: 16px;
+  vertical-align: middle;
+  cursor: pointer;
+}
+
+.settings-icon {
+  font-size: 18px;
+}
+
+.content {
+  flex: 1;
+  padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+  .container {
+    background-repeat: no-repeat;
+    background-position: center 110px;
+    background-size: 100%;
+  }
+
+  .content {
+    padding: 32px 0 24px;
+  }
+}
+
+.top {
+  text-align: center;
+}
+
+.header {
+  height: 44px;
+  line-height: 44px;
+
+  a {
+    text-decoration: none;
+  }
+}
+
+.logo {
+  height: 44px;
+  margin-right: 16px;
+  vertical-align: top;
+}
+
+.title {
+  position: relative;
+  top: 2px;
+  color: @heading-color;
+  font-weight: 600;
+  font-size: 33px;
+  font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+  margin-top: 12px;
+  margin-bottom: 40px;
+  color: @text-color-secondary;
+  font-size: @font-size-base;
+}
+
+.main {
+  width: 368px;
+  margin: 0 auto;
+  @media screen and (max-width: @screen-sm) {
+    width: 95%;
+  }
+
+  .icon {
+    margin-left: 16px;
+    color: rgba(0, 0, 0, 0.2);
+    font-size: 24px;
+    vertical-align: middle;
+    cursor: pointer;
+    transition: color 0.3s;
+
+    &:hover {
+      color: @primary-color;
+    }
+  }
+
+  .submit {
+    width: 100%;
+  }
+
+  .other {
+    margin-top: 24px;
+    line-height: 22px;
+    text-align: left;
+
+    .register {
+      float: right;
+    }
+  }
+}
diff --git a/src/pages/User/Login.tsx b/src/pages/User/Login.tsx
new file mode 100644
index 0000000..56542d5
--- /dev/null
+++ b/src/pages/User/Login.tsx
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useState } from 'react';
+import { Button, notification, Tabs } from 'antd';
+import { DefaultFooter } from '@ant-design/pro-layout';
+import { SelectLang } from '@@/plugin-locale/SelectLang';
+import { Link, useIntl, history } from 'umi';
+import LoginMethodPassword from '@/pages/User/components/LoginMethodPassword';
+import LoginMethodExample from '@/pages/User/components/LoginMethodExample';
+import { UserModule } from '@/pages/User/typing';
+import logo from '@/assets/logo.svg';
+import { SettingOutlined } from '@ant-design/icons';
+import styles from './Login.less';
+import { getUrlQuery } from '@/helpers';
+
+const Tab = Tabs.TabPane;
+
+/**
+ * Login Methods List
+ */
+const loginMethods: UserModule.LoginMethod[] = [LoginMethodPassword, LoginMethodExample];
+
+/**
+ * User Login Page
+ * @constructor
+ */
+const Page: React.FC = () => {
+  const { formatMessage } = useIntl();
+  const [loginMethod, setLoginMethod] = useState(loginMethods[0]);
+
+  const onSettingsClick = () => {
+    history.replace(`/settings?redirect=${encodeURIComponent(history.location.pathname)}`);
+  };
+
+  const onTabChange = (activeKey: string) => {
+    loginMethods.forEach((item, index) => {
+      if (activeKey === item.id) setLoginMethod(loginMethods[index]);
+    });
+  };
+
+  const onSubmit = () => {
+    loginMethod.checkData().then((validate) => {
+      if (validate) {
+        loginMethod.submit(loginMethod.getData()).then((response) => {
+          if (response.status) {
+            notification.success({
+              message: formatMessage({ id: 'component.status.success' }),
+              description: response.message,
+              duration: 1,
+              onClose: () => {
+                const redirect = getUrlQuery('redirect');
+                history.replace(redirect ? decodeURIComponent(redirect) : '/');
+              },
+            });
+          } else {
+            notification.error({
+              message: formatMessage({ id: 'component.status.fail' }),
+              description: response.message,
+            });
+          }
+        });
+      }
+    });
+  };
+
+  if (localStorage.getItem('token')) {
+    history.replace('/');
+    return null;
+  }
+  return (
+    <div className={styles.container}>
+      <div className={styles.lang}>
+        <div className={styles.settings} onClick={onSettingsClick}>
+          <SettingOutlined />
+        </div>
+        <SelectLang />
+      </div>
+      <div className={styles.content}>
+        <div className={styles.top}>
+          <div className={styles.header}>
+            <Link to="/">
+              <img alt="logo" className={styles.logo} src={logo} />
+            </Link>
+          </div>
+          <div className={styles.desc}>
+            Apache APISIX Dashboard
+            <br />
+            Cloud-Native Microservices API Gateway
+          </div>
+        </div>
+        <div className={styles.main}>
+          <Tabs activeKey={loginMethod.id} onChange={onTabChange}>
+            {loginMethods.map((item) => (
+              <Tab key={item.id} tab={item.name}>
+                {item.render()}
+              </Tab>
+            ))}
+          </Tabs>
+          <Button className={styles.submit} size="large" type="primary" onClick={onSubmit}>
+            {formatMessage({ id: 'component.user.login' })}
+          </Button>
+        </div>
+      </div>
+      <DefaultFooter />
+    </div>
+  );
+};
+
+export default Page;
diff --git a/api/route/base_test.go b/src/pages/User/Logout.tsx
similarity index 52%
copy from api/route/base_test.go
copy to src/pages/User/Logout.tsx
index 3e3a096..9f64c2a 100644
--- a/api/route/base_test.go
+++ b/src/pages/User/Logout.tsx
@@ -14,25 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package route
+import React from 'react';
+import LoginMethodPassword from '@/pages/User/components/LoginMethodPassword';
+import LoginMethodExample from '@/pages/User/components/LoginMethodExample';
+import { UserModule } from '@/pages/User/typing';
+import { getUrlQuery } from '@/helpers';
 
-import (
-	"github.com/api7/apitest"
-
-	"github.com/apisix/manager-api/conf"
-)
-
-var handler *apitest.APITest
+/**
+ * Login Methods List
+ */
+const loginMethods: UserModule.LoginMethod[] = [LoginMethodPassword, LoginMethodExample];
 
-var (
-	uriPrefix = "/apisix/admin"
-)
+/**
+ * User Logout Page
+ * @constructor
+ */
+const Page: React.FC = () => {
+  // run all logout method
+  loginMethods.forEach((item) => {
+    item.logout();
+  });
 
-func init() {
-	//init mysql connect
-	conf.InitializeMysql()
+  const redirect = getUrlQuery('redirect');
+  window.location.href = `/user/login${redirect ? `?redirect=${redirect}` : ''}`;
 
-	r := SetUpRouter()
+  return null;
+};
 
-	handler = apitest.New().Handler(r)
-}
+export default Page;
diff --git a/api/route/base_test.go b/src/pages/User/components/LoginMethodExample.tsx
similarity index 53%
copy from api/route/base_test.go
copy to src/pages/User/components/LoginMethodExample.tsx
index 3e3a096..305ec7a 100644
--- a/api/route/base_test.go
+++ b/src/pages/User/components/LoginMethodExample.tsx
@@ -14,25 +14,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package route
+import React from 'react';
+import { UserModule } from '@/pages/User/typing';
+import { formatMessage } from '@@/plugin-locale/localeExports';
 
-import (
-	"github.com/api7/apitest"
+const LoginMethodExample: UserModule.LoginMethod = {
+  id: 'example',
+  name: formatMessage({ id: 'component.user.loginMethodExample' }),
+  render: () => {
+    return <a href="https://www.example.com">example</a>;
+  },
+  getData(): UserModule.LoginData {
+    return {};
+  },
+  checkData: async () => {
+    return true;
+  },
+  submit: async (data) => {
+    return {
+      status: false,
+      message: formatMessage({ id: 'component.user.loginMethodExample.message' }),
+      data,
+    };
+  },
+  logout() {},
+};
 
-	"github.com/apisix/manager-api/conf"
-)
-
-var handler *apitest.APITest
-
-var (
-	uriPrefix = "/apisix/admin"
-)
-
-func init() {
-	//init mysql connect
-	conf.InitializeMysql()
-
-	r := SetUpRouter()
-
-	handler = apitest.New().Handler(r)
-}
+export default LoginMethodExample;
diff --git a/src/pages/User/components/LoginMethodPassword.tsx b/src/pages/User/components/LoginMethodPassword.tsx
new file mode 100644
index 0000000..698addc
--- /dev/null
+++ b/src/pages/User/components/LoginMethodPassword.tsx
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { UserModule } from '@/pages/User/typing';
+import { Form, Input } from 'antd';
+import { FormInstance } from 'antd/lib/form';
+import { UserOutlined, LockTwoTone } from '@ant-design/icons';
+import { formatMessage } from '@@/plugin-locale/localeExports';
+import { request } from '@@/plugin-request/request';
+
+const formRef = React.createRef<FormInstance>();
+
+const LoginMethodPassword: UserModule.LoginMethod = {
+  id: 'password',
+  name: formatMessage({ id: 'component.user.loginMethodPassword' }),
+  render: () => {
+    return (
+      <Form ref={formRef} name="control-ref">
+        <Form.Item
+          name="username"
+          rules={[
+            {
+              required: true,
+              message: formatMessage({ id: 'component.user.loginMethodPassword.inputUsername' }),
+            },
+          ]}
+        >
+          <Input
+            size="large"
+            type="text"
+            placeholder={formatMessage({ id: 'component.user.loginMethodPassword.username' })}
+            prefix={
+              <UserOutlined
+                style={{
+                  color: '#1890ff',
+                }}
+              />
+            }
+          />
+        </Form.Item>
+        <Form.Item
+          name="password"
+          rules={[
+            {
+              required: true,
+              message: formatMessage({ id: 'component.user.loginMethodPassword.inputPassword' }),
+            },
+          ]}
+        >
+          <Input
+            size="large"
+            type="password"
+            placeholder={formatMessage({ id: 'component.user.loginMethodPassword.password' })}
+            prefix={<LockTwoTone />}
+          />
+        </Form.Item>
+      </Form>
+    );
+  },
+  getData(): UserModule.LoginData {
+    if (formRef.current) {
+      const data = formRef.current.getFieldsValue();
+      return {
+        username: data.username,
+        password: data.password,
+      };
+    }
+    return {};
+  },
+  checkData: async () => {
+    if (formRef.current) {
+      try {
+        await formRef.current.validateFields();
+        return true;
+      } catch (e) {
+        return false;
+      }
+    }
+    return false;
+  },
+  submit: async (data) => {
+    if (data.username !== '' && data.password !== '') {
+      try {
+        const result = await request('/user/login', {
+          method: 'POST',
+          requestType: 'form',
+          prefix: 'http://localhost:8080',
+          data: {
+            username: data.username,
+            password: data.password,
+          },
+        });
+
+        localStorage.setItem('token', result.data.token);
+        return {
+          status: true,
+          message: formatMessage({ id: 'component.user.loginMethodPassword.success' }),
+          data: [],
+        };
+      } catch (e) {
+        return {
+          status: false,
+          message: formatMessage({ id: 'component.user.loginMethodPassword.incorrectPassword' }),
+          data: [],
+        };
+      }
+    } else {
+      return {
+        status: false,
+        message: formatMessage({ id: 'component.user.loginMethodPassword.fieldInvalid' }),
+        data: [],
+      };
+    }
+  },
+  logout: () => {
+    console.log('password logout');
+    localStorage.removeItem('token');
+  },
+};
+
+export default LoginMethodPassword;
diff --git a/src/pages/User/index.ts b/src/pages/User/index.ts
new file mode 100644
index 0000000..3ee57c2
--- /dev/null
+++ b/src/pages/User/index.ts
@@ -0,0 +1,2 @@
+export { default as UserZhCN } from './locales/zh-CN';
+export { default as UserEnUS } from './locales/en-US';
diff --git a/src/pages/User/locales/en-US.ts b/src/pages/User/locales/en-US.ts
new file mode 100644
index 0000000..bfcadc0
--- /dev/null
+++ b/src/pages/User/locales/en-US.ts
@@ -0,0 +1,14 @@
+export default {
+  'component.user.login': 'Login',
+  'component.user.loginMethodPassword': 'Username & Password',
+  'component.user.loginMethodPassword.username': 'Username',
+  'component.user.loginMethodPassword.password': 'Password',
+  'component.user.loginMethodPassword.inputUsername': 'Please input username',
+  'component.user.loginMethodPassword.inputPassword': 'Please input password',
+  'component.user.loginMethodPassword.incorrectPassword': 'Incorrect username or password',
+  'component.user.loginMethodPassword.fieldInvalid': 'Please check username and password',
+  'component.user.loginMethodPassword.success': 'Login Success',
+  'component.user.loginMethodExample': 'Example',
+  'component.user.loginMethodExample.message':
+    'Example Login Method, It is only used as an extension example of login method and cannot be used.',
+};
diff --git a/src/pages/User/locales/zh-CN.ts b/src/pages/User/locales/zh-CN.ts
new file mode 100644
index 0000000..d48e5bb
--- /dev/null
+++ b/src/pages/User/locales/zh-CN.ts
@@ -0,0 +1,13 @@
+export default {
+  'component.user.login': '登录',
+  'component.user.loginMethodPassword': '账号密码登录',
+  'component.user.loginMethodPassword.username': '账号',
+  'component.user.loginMethodPassword.password': '密码',
+  'component.user.loginMethodPassword.inputUsername': '请输入账号!',
+  'component.user.loginMethodPassword.inputPassword': '请输入密码!',
+  'component.user.loginMethodPassword.incorrectPassword': '账号或密码错误',
+  'component.user.loginMethodPassword.fieldInvalid': '请检查账号和密码',
+  'component.user.loginMethodPassword.success': '登录成功',
+  'component.user.loginMethodExample': '示例登录',
+  'component.user.loginMethodExample.message': '示例登录方式,仅作为登录方式扩展例子,无法使用',
+};
diff --git a/src/pages/User/typing.d.ts b/src/pages/User/typing.d.ts
new file mode 100644
index 0000000..3f7d063
--- /dev/null
+++ b/src/pages/User/typing.d.ts
@@ -0,0 +1,25 @@
+import React from 'react';
+
+declare namespace UserModule {
+  interface LoginMethod {
+    id: string;
+    name: string;
+    render: () => React.ReactElement;
+    getData: () => LoginData;
+    checkData: () => Promise<boolean>;
+    submit: (data) => Promise<LoginResponse>;
+    logout: () => void;
+  }
+
+  type LoginData = {
+    [string]: string;
+  };
+
+  interface LoginResponse {
+    status: boolean;
+    message: string;
+    data: {
+      [string]: any;
+    };
+  }
+}