fix: 管理端改为账号密码登录 (#29)
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
)
|
||||
|
||||
func TestSystemRouterExposesLoginWithoutCaptcha(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
InitSysRouter(engine, &jwt.GinJWTMiddleware{})
|
||||
|
||||
routes := make(map[string]struct{})
|
||||
for _, route := range engine.Routes() {
|
||||
routes[route.Method+" "+route.Path] = struct{}{}
|
||||
}
|
||||
if _, ok := routes["POST /api/v1/login"]; !ok {
|
||||
t.Fatal("password login route is not registered")
|
||||
}
|
||||
for _, path := range []string{"GET /api/v1/captcha", "GET /api/v1/getCaptcha"} {
|
||||
if _, ok := routes[path]; ok {
|
||||
t.Fatalf("captcha route must not be registered: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestPasswordOnlyLoginHandlerMySQL(t *testing.T) {
|
||||
db := openLoginIntegrationDB(t)
|
||||
username := fmt.Sprintf("login-test-%d", time.Now().UnixNano())
|
||||
password := "synthetic-login-password"
|
||||
seedLoginIntegrationUser(t, db, username, password)
|
||||
t.Cleanup(func() {
|
||||
if err := db.Exec(`DELETE FROM sys_user WHERE username = ?`, username).Error; err != nil {
|
||||
t.Errorf("delete login test user: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
originalApplication := *config.ApplicationConfig
|
||||
originalJWT := *config.JwtConfig
|
||||
originalLogger := *config.LoggerConfig
|
||||
config.ApplicationConfig.Mode = "prod"
|
||||
config.JwtConfig.Secret = "integration-test-jwt-secret"
|
||||
config.JwtConfig.Timeout = 60
|
||||
config.LoggerConfig.EnabledDB = false
|
||||
t.Cleanup(func() {
|
||||
*config.ApplicationConfig = originalApplication
|
||||
*config.JwtConfig = originalJWT
|
||||
*config.LoggerConfig = originalLogger
|
||||
})
|
||||
|
||||
auth, err := AuthInit()
|
||||
if err != nil {
|
||||
t.Fatalf("initialize test JWT middleware: %v", err)
|
||||
}
|
||||
engine := gin.New()
|
||||
engine.POST("/api/v1/login", func(c *gin.Context) {
|
||||
c.Set("db", db)
|
||||
c.Next()
|
||||
}, auth.LoginHandler)
|
||||
|
||||
response := loginRequest(t, engine, fmt.Sprintf(`{"username":%q,"password":%q}`, username, password))
|
||||
if response.Code != http.StatusOK || response.Token == "" {
|
||||
t.Fatal("production password-only login did not return an authenticated response")
|
||||
}
|
||||
|
||||
for _, payload := range []string{
|
||||
fmt.Sprintf(`{"username":%q}`, username),
|
||||
fmt.Sprintf(`{"username":%q,"password":"wrong-password"}`, username),
|
||||
} {
|
||||
response := loginRequest(t, engine, payload)
|
||||
if response.Code == http.StatusOK || response.Token != "" {
|
||||
t.Fatal("invalid password-only login was not rejected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type loginHTTPResponse struct {
|
||||
Code int `json:"code"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func loginRequest(t *testing.T, engine *gin.Engine, payload string) loginHTTPResponse {
|
||||
t.Helper()
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/login", bytes.NewBufferString(payload))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
engine.ServeHTTP(recorder, request)
|
||||
var response loginHTTPResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("decode login response: %v", err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func openLoginIntegrationDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
if os.Getenv("CHORUS_RUN_ADMIN_TESTS") != "1" {
|
||||
t.Skip("set CHORUS_RUN_ADMIN_TESTS=1 to run against the disposable MySQL test database")
|
||||
}
|
||||
dsn := strings.TrimSpace(os.Getenv("CHORUS_DSN"))
|
||||
if dsn == "" {
|
||||
t.Fatal("CHORUS_DSN is required for login integration tests")
|
||||
}
|
||||
parsed, err := mysqldriver.ParseDSN(dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("parse MySQL DSN: %v", err)
|
||||
}
|
||||
if parsed.DBName != "chorus_test" {
|
||||
t.Fatalf("refusing to use non-disposable database %q", parsed.DBName)
|
||||
}
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
if err != nil {
|
||||
t.Fatalf("open login integration database: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func seedLoginIntegrationUser(t *testing.T, db *gorm.DB, username, password string) {
|
||||
t.Helper()
|
||||
var roleID int64
|
||||
if err := db.Raw(`SELECT role_id FROM sys_role WHERE role_key = 'chorus_operator' AND status = '2' AND deleted_at IS NULL`).Scan(&roleID).Error; err != nil || roleID == 0 {
|
||||
t.Fatalf("find active chorus operator role: %v", err)
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
t.Fatalf("hash login test password: %v", err)
|
||||
}
|
||||
if err := db.Exec(`INSERT INTO sys_user (username, password, role_id, status) VALUES (?, ?, ?, '2')`, username, string(hash), roleID).Error; err != nil {
|
||||
t.Fatalf("seed login test user: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,16 @@ import (
|
||||
"git.ilapage.cn/OPC/chorus/admin/common"
|
||||
"net/http"
|
||||
|
||||
"git.ilapage.cn/OPC/chorus/admin/common/global"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/captcha"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/mssola/user_agent"
|
||||
"git.ilapage.cn/OPC/chorus/admin/common/global"
|
||||
)
|
||||
|
||||
func PayloadFunc(data interface{}) jwt.MapClaims {
|
||||
@@ -52,8 +51,7 @@ func IdentityHandler(c *gin.Context) interface{} {
|
||||
// @Description LoginHandler can be used by clients to get a jwt token.
|
||||
// @Description Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}.
|
||||
// @Description Reply will be of the form {"token": "TOKEN"}.
|
||||
// @Description dev mode:It should be noted that all fields cannot be empty, and a value of 0 can be passed in addition to the account password
|
||||
// @Description 注意:开发模式:需要注意全部字段不能为空,账号密码外可以传入0值
|
||||
// @Description The request requires only username and password in every application mode.
|
||||
// @Tags 登陆
|
||||
// @Accept application/json
|
||||
// @Product application/json
|
||||
@@ -84,15 +82,6 @@ func Authenticator(c *gin.Context) (interface{}, error) {
|
||||
|
||||
return nil, jwt.ErrMissingLoginValues
|
||||
}
|
||||
if config.ApplicationConfig.Mode != "dev" {
|
||||
if !captcha.Verify(loginVals.UUID, loginVals.Code, true) {
|
||||
username = loginVals.Username
|
||||
msg = "验证码错误"
|
||||
status = "1"
|
||||
|
||||
return nil, jwt.ErrInvalidVerificationode
|
||||
}
|
||||
}
|
||||
sysUser, role, e := loginVals.GetUser(db)
|
||||
if e == nil {
|
||||
username = loginVals.Username
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestAuthenticatorAllowsPasswordOnlyInProduction(t *testing.T) {
|
||||
db := loginTestDB(t)
|
||||
seedLoginUser(t, db, "operator", "correct-password")
|
||||
|
||||
originalMode := config.ApplicationConfig.Mode
|
||||
config.ApplicationConfig.Mode = "prod"
|
||||
t.Cleanup(func() { config.ApplicationConfig.Mode = originalMode })
|
||||
|
||||
identity, err := authenticateLoginRequest(t, db, `{"username":"operator","password":"correct-password"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("authenticate production password-only login: %v", err)
|
||||
}
|
||||
claims, ok := identity.(map[string]interface{})
|
||||
if !ok || claims["user"] == nil || claims["role"] == nil {
|
||||
t.Fatalf("unexpected authenticated identity: %#v", identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticatorRejectsMissingAndIncorrectPassword(t *testing.T) {
|
||||
db := loginTestDB(t)
|
||||
seedLoginUser(t, db, "operator", "correct-password")
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
payload string
|
||||
want error
|
||||
}{
|
||||
{name: "missing password", payload: `{"username":"operator"}`, want: jwt.ErrMissingLoginValues},
|
||||
{name: "wrong password", payload: `{"username":"operator","password":"wrong-password"}`, want: jwt.ErrFailedAuthentication},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := authenticateLoginRequest(t, db, test.payload)
|
||||
if !errors.Is(err, test.want) {
|
||||
t.Fatalf("authentication error = %v, want %v", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func authenticateLoginRequest(t *testing.T, db *gorm.DB, payload string) (interface{}, error) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
context, _ := gin.CreateTestContext(recorder)
|
||||
request := httptest.NewRequest("POST", "/api/v1/login", bytes.NewBufferString(payload))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
context.Request = request
|
||||
context.Set("db", db)
|
||||
return Authenticator(context)
|
||||
}
|
||||
|
||||
func loginTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open login test database: %v", err)
|
||||
}
|
||||
for _, statement := range []string{
|
||||
`CREATE TABLE sys_role (role_id INTEGER PRIMARY KEY, role_name TEXT NOT NULL, status TEXT NOT NULL, role_key TEXT NOT NULL, data_scope TEXT NOT NULL, deleted_at DATETIME NULL)`,
|
||||
`CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, username TEXT NOT NULL, password TEXT NOT NULL, role_id INTEGER NOT NULL, status TEXT NOT NULL, deleted_at DATETIME NULL)`,
|
||||
} {
|
||||
if err := db.Exec(statement).Error; err != nil {
|
||||
t.Fatalf("create login test schema: %v", err)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func seedLoginUser(t *testing.T, db *gorm.DB, username, password string) {
|
||||
t.Helper()
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
t.Fatalf("hash test password: %v", err)
|
||||
}
|
||||
if err := db.Exec(`INSERT INTO sys_role (role_id, role_name, status, role_key, data_scope) VALUES (1, 'operator', '2', 'chorus_operator', 'all')`).Error; err != nil {
|
||||
t.Fatalf("seed login role: %v", err)
|
||||
}
|
||||
if err := db.Exec(`INSERT INTO sys_user (user_id, username, password, role_id, status) VALUES (1, ?, ?, 1, '2')`, username, string(hash)).Error; err != nil {
|
||||
t.Fatalf("seed login user: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
type Login struct {
|
||||
Username string `form:"UserName" json:"username" binding:"required"`
|
||||
Password string `form:"Password" json:"password" binding:"required"`
|
||||
Code string `form:"Code" json:"code" binding:"required"`
|
||||
UUID string `form:"UUID" json:"uuid" binding:"required"`
|
||||
}
|
||||
|
||||
func (u *Login) GetUser(tx *gorm.DB) (user SysUser, role SysRole, err error) {
|
||||
|
||||
Reference in New Issue
Block a user