From 7bc716cff6b41aa34546ed583b846ee33f666d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 3 Dec 2024 03:46:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 - go.sum | 54 ---------------- internal/app/global.go | 2 +- internal/http/middleware/must_login.go | 20 ++++++ internal/http/request/user.go | 5 +- internal/route/http.go | 1 + internal/service/dashboard.go | 3 - internal/service/user.go | 66 +++++++++++++++++++- pkg/rsacrypto/rsacrypto.go | 85 ++++++++++++++++++++++++++ pkg/rsacrypto/rsacrypto_test.go | 39 ++++++++++++ web/package.json | 2 + web/pnpm-lock.yaml | 34 +++++++++++ web/src/api/panel/user/index.ts | 16 ++--- web/src/utils/encrypt/index.ts | 1 + web/src/utils/encrypt/rsa.ts | 16 +++++ web/src/views/login/IndexView.vue | 82 ++++++++++++++----------- 16 files changed, 322 insertions(+), 105 deletions(-) create mode 100644 pkg/rsacrypto/rsacrypto.go create mode 100644 pkg/rsacrypto/rsacrypto_test.go create mode 100644 web/src/utils/encrypt/index.ts create mode 100644 web/src/utils/encrypt/rsa.ts diff --git a/go.mod b/go.mod index c5af2f4484..37be5b06ab 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a github.com/gomodule/redigo v1.9.2 - github.com/google/wire v0.6.0 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-version v1.7.0 github.com/knadh/koanf/parsers/yaml v0.1.0 diff --git a/go.sum b/go.sum index a45be40f26..1c7be1b65c 100644 --- a/go.sum +++ b/go.sum @@ -62,18 +62,14 @@ github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a h1:BAyyIK6rc6Tq9 github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a/go.mod h1:bgk4Ij/0OQ89UeoFFAQrSNhbbr4rKJ0fwWfo7wc+TCc= github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -155,7 +151,6 @@ github.com/tufanbarisyildirim/gonginx v0.0.0-20241115180907-128af6df1765 h1:nnw6 github.com/tufanbarisyildirim/gonginx v0.0.0-20241115180907-128af6df1765/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -166,77 +161,28 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/app/global.go b/internal/app/global.go index f30f191658..2b266a66c2 100644 --- a/internal/app/global.go +++ b/internal/app/global.go @@ -48,7 +48,7 @@ var ( // 自动注入 var ( - Version string + Version = "0.0.0" BuildTime string CommitHash string GoVersion string diff --git a/internal/http/middleware/must_login.go b/internal/http/middleware/must_login.go index f91ef4fb5a..9879b9ed83 100644 --- a/internal/http/middleware/must_login.go +++ b/internal/http/middleware/must_login.go @@ -2,12 +2,15 @@ package middleware import ( "context" + "fmt" + "net" "net/http" "slices" "strings" "github.com/go-rat/chix" "github.com/spf13/cast" + "golang.org/x/crypto/sha3" "github.com/TheTNB/panel/internal/app" ) @@ -16,6 +19,7 @@ import ( func MustLogin(next http.Handler) http.Handler { // 白名单 whiteList := []string{ + "/api/user/key", "/api/user/login", "/api/user/logout", "/api/user/isLogin", @@ -57,6 +61,22 @@ func MustLogin(next http.Handler) http.Handler { return } + safeLogin := cast.ToBool(sess.Get("safe_login")) + if safeLogin { + safeClientHash := cast.ToString(sess.Get("safe_client")) + ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) + ua := r.Header.Get("User-Agent") + clientHash := fmt.Sprintf("%x", sha3.Sum256([]byte(ip+"|"+ua))) + if safeClientHash != clientHash || safeClientHash == "" { + render := chix.NewRender(w) + render.Status(http.StatusUnauthorized) + render.JSON(chix.M{ + "message": "客户端IP/UA变化,请重新登录", + }) + return + } + } + r = r.WithContext(context.WithValue(r.Context(), "user_id", userID)) // nolint:staticcheck next.ServeHTTP(w, r) }) diff --git a/internal/http/request/user.go b/internal/http/request/user.go index 8f4e033552..59dbf7af41 100644 --- a/internal/http/request/user.go +++ b/internal/http/request/user.go @@ -1,6 +1,7 @@ package request type UserLogin struct { - Username string `json:"username" form:"username" validate:"required,min=3,max=255"` - Password string `json:"password" form:"password" validate:"required,min=6,max=255"` + Username string `json:"username" form:"username" validate:"required"` + Password string `json:"password" form:"password" validate:"required"` + SafeLogin bool `json:"safe_login" form:"safe_login"` } diff --git a/internal/route/http.go b/internal/route/http.go index e9d554326b..8a7194b50a 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -18,6 +18,7 @@ func Http(r chi.Router) { r.Route("/api", func(r chi.Router) { r.Route("/user", func(r chi.Router) { user := service.NewUserService() + r.Get("/key", user.GetKey) r.With(middleware.Throttle(5, time.Minute)).Post("/login", user.Login) r.Post("/logout", user.Logout) r.Get("/isLogin", user.IsLogin) diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go index f04ca7c6e5..bb02be8922 100644 --- a/internal/service/dashboard.go +++ b/internal/service/dashboard.go @@ -6,7 +6,6 @@ import ( "net/http" "regexp" "strings" - "time" "github.com/go-rat/chix" "github.com/go-rat/utils/collect" @@ -104,8 +103,6 @@ func (s *DashboardService) SystemInfo(w http.ResponseWriter, r *http.Request) { }) } - time.Now().UTC() - Success(w, chix.M{ "procs": hostInfo.Procs, "hostname": hostInfo.Hostname, diff --git a/internal/service/user.go b/internal/service/user.go index 7ac764fffb..854581ac03 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -1,15 +1,22 @@ package service import ( + "crypto/rsa" + "encoding/json" + "fmt" + "net" "net/http" + "strings" "github.com/go-rat/chix" "github.com/spf13/cast" + "golang.org/x/crypto/sha3" "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/rsacrypto" ) type UserService struct { @@ -22,6 +29,35 @@ func NewUserService() *UserService { } } +func (s *UserService) GetKey(w http.ResponseWriter, r *http.Request) { + key, err := rsacrypto.GenerateKey() + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + sess, err := app.Session.GetSession(r) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + encoded, err := json.Marshal(key) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + sess.Put("key", encoded) + + pk, err := rsacrypto.PublicKeyToString(&key.PublicKey) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, pk) +} + func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { sess, err := app.Session.GetSession(r) if err != nil { @@ -35,21 +71,47 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { return } - user, err := s.repo.CheckPassword(req.Username, req.Password) + key := new(rsa.PrivateKey) + if err = json.Unmarshal(sess.Get("key").([]byte), key); err != nil { + Error(w, http.StatusForbidden, "invalid key, please refresh the page") + return + } + + decryptedUsername, _ := rsacrypto.DecryptData(key, req.Username) + decryptedPassword, _ := rsacrypto.DecryptData(key, req.Password) + user, err := s.repo.CheckPassword(string(decryptedUsername), string(decryptedPassword)) if err != nil { Error(w, http.StatusForbidden, "%v", err) return } + // 安全登录模式下,将当前客户端与会话绑定 + // 安全登录模式只在未启用TLS时生效,因为TLS本身就是安全的 + ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + if req.SafeLogin && !app.Conf.Bool("http.tls") { + ua := r.Header.Get("User-Agent") + sess.Put("safe_login", true) + sess.Put("safe_client", fmt.Sprintf("%x", sha3.Sum256([]byte(ip+"|"+ua)))) + } + sess.Put("user_id", user.ID) + sess.Forget("key") Success(w, nil) } func (s *UserService) Logout(w http.ResponseWriter, r *http.Request) { sess, err := app.Session.GetSession(r) if err == nil { - sess.Forget("user_id") + if err = sess.Invalidate(); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } } + Success(w, nil) } diff --git a/pkg/rsacrypto/rsacrypto.go b/pkg/rsacrypto/rsacrypto.go new file mode 100644 index 0000000000..74353d8d76 --- /dev/null +++ b/pkg/rsacrypto/rsacrypto.go @@ -0,0 +1,85 @@ +package rsacrypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" +) + +const ( + keySize = 2048 // RSA key size in bits +) + +// GenerateKey 生成RSA密钥对 +func GenerateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, keySize) +} + +// EncryptData 加密数据 +func EncryptData(publicKey *rsa.PublicKey, data []byte) (string, error) { + ciphertext, err := rsa.EncryptOAEP( + sha512.New(), + rand.Reader, + publicKey, + data, + nil, + ) + if err != nil { + return "", fmt.Errorf("encryption failed: %v", err) + } + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// DecryptData 解密数据 +func DecryptData(privateKey *rsa.PrivateKey, ciphertext string) ([]byte, error) { + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return nil, fmt.Errorf("failed to decode base64: %v", err) + } + + plaintext, err := rsa.DecryptOAEP( + sha512.New(), + rand.Reader, + privateKey, + data, + nil, + ) + if err != nil { + return nil, fmt.Errorf("decryption failed: %v", err) + } + + return plaintext, nil +} + +// PrivateKeyToString 将RSA私钥转换为PEM格式的字符串 +func PrivateKeyToString(privateKey *rsa.PrivateKey) (string, error) { + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }, + ) + return string(privateKeyPEM), nil +} + +// PublicKeyToString 将RSA公钥转换为PEM格式的字符串 +func PublicKeyToString(publicKey *rsa.PublicKey) (string, error) { + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", fmt.Errorf("failed to marshal public key: %v", err) + } + + publicKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + }, + ) + return string(publicKeyPEM), nil +} diff --git a/pkg/rsacrypto/rsacrypto_test.go b/pkg/rsacrypto/rsacrypto_test.go new file mode 100644 index 0000000000..8a4809482b --- /dev/null +++ b/pkg/rsacrypto/rsacrypto_test.go @@ -0,0 +1,39 @@ +package rsacrypto + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type RSATestSuite struct { + suite.Suite +} + +func TestRSATestSuite(t *testing.T) { + suite.Run(t, &RSATestSuite{}) +} + +func (suite *RSATestSuite) TestRSA() { + // 生成RSA密钥对 + privateKey, err := GenerateKey() + suite.NoError(err) + suite.NotEmpty(privateKey) + suite.NotEmpty(privateKey.PublicKey) + + // 提取密钥对 + suite.NotEmpty(PrivateKeyToString(privateKey)) + suite.NotEmpty(PublicKeyToString(&privateKey.PublicKey)) + + message := []byte("Rat Panel") + + // 加密数据 + ciphertext, err := EncryptData(&privateKey.PublicKey, message) + suite.NoError(err) + suite.NotEmpty(ciphertext) + + // 解密数据 + decrypted, err := DecryptData(privateKey, ciphertext) + suite.NoError(err) + suite.NotEmpty(decrypted) +} diff --git a/web/package.json b/web/package.json index 419a5f81c9..0f36d50c05 100644 --- a/web/package.json +++ b/web/package.json @@ -41,6 +41,7 @@ "luxon": "^3.5.0", "marked": "^15.0.2", "mitt": "^3.0.1", + "node-forge": "^1.3.1", "pinia": "^2.2.6", "pinia-plugin-persistedstate": "^4.1.3", "remove": "^0.1.5", @@ -57,6 +58,7 @@ "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", "@types/node": "^22.9.1", + "@types/node-forge": "^1.3.11", "@unocss/eslint-config": "^0.65.0", "@vitejs/plugin-vue": "^5.2.0", "@vue/eslint-config-prettier": "^10.1.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 347a766b30..3613e49cb5 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: mitt: specifier: ^3.0.1 version: 3.0.1 + node-forge: + specifier: ^1.3.1 + version: 1.3.1 pinia: specifier: ^2.2.6 version: 2.2.8(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) @@ -114,6 +117,9 @@ importers: '@types/node': specifier: ^22.9.1 version: 22.10.1 + '@types/node-forge': + specifier: ^1.3.11 + version: 1.3.11 '@unocss/eslint-config': specifier: ^0.65.0 version: 0.65.0(eslint@9.16.0(jiti@2.4.1))(typescript@5.6.3) @@ -966,36 +972,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.0': resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.0': resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.0': resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.0': resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.0': resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.0': resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} @@ -1073,46 +1085,55 @@ packages: resolution: {integrity: sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.28.0': resolution: {integrity: sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.28.0': resolution: {integrity: sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.28.0': resolution: {integrity: sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.28.0': resolution: {integrity: sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.28.0': resolution: {integrity: sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.28.0': resolution: {integrity: sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.28.0': resolution: {integrity: sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.28.0': resolution: {integrity: sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.28.0': resolution: {integrity: sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==} @@ -1173,6 +1194,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} @@ -2705,6 +2729,10 @@ packages: node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-html-parser@5.4.2: resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==} @@ -4641,6 +4669,10 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 22.10.1 + '@types/node@22.10.1': dependencies: undici-types: 6.20.0 @@ -6442,6 +6474,8 @@ snapshots: node-fetch-native@1.6.4: {} + node-forge@1.3.1: {} + node-html-parser@5.4.2: dependencies: css-select: 4.3.0 diff --git a/web/src/api/panel/user/index.ts b/web/src/api/panel/user/index.ts index d48581e947..adb55c9993 100644 --- a/web/src/api/panel/user/index.ts +++ b/web/src/api/panel/user/index.ts @@ -1,18 +1,18 @@ -import type { AxiosResponse } from 'axios' - -import { request } from '@/utils' +import { http } from '@/utils' export default { + // 公钥 + key: () => http.Get('/user/key'), // 登录 - login: (username: string, password: string): Promise> => - request.post('/user/login', { + login: (username: string, password: string) => + http.Post('/user/login', { username, password }), // 登出 - logout: (): Promise> => request.post('/user/logout'), + logout: () => http.Post('/user/logout'), // 是否登录 - isLogin: (): Promise> => request.get('/user/isLogin'), + isLogin: () => http.Get('/user/isLogin'), // 获取用户信息 - info: (): Promise> => request.get('/user/info') + info: () => http.Get('/user/info') } diff --git a/web/src/utils/encrypt/index.ts b/web/src/utils/encrypt/index.ts new file mode 100644 index 0000000000..b76e15bf59 --- /dev/null +++ b/web/src/utils/encrypt/index.ts @@ -0,0 +1 @@ +export * from './rsa' diff --git a/web/src/utils/encrypt/rsa.ts b/web/src/utils/encrypt/rsa.ts new file mode 100644 index 0000000000..f20c105d0b --- /dev/null +++ b/web/src/utils/encrypt/rsa.ts @@ -0,0 +1,16 @@ +import * as forge from 'node-forge' + +export function rsaEncrypt(data: string, publicKey: string) { + const pk = forge.pki.publicKeyFromPem(publicKey) + const encryptedBytes = pk.encrypt(data, 'RSA-OAEP', { + md: forge.md.sha512.create() + }) + return forge.util.encode64(encryptedBytes) +} + +export function rsaDecrypt(data: string, privateKey: string) { + const pk = forge.pki.privateKeyFromPem(privateKey) + return pk.decrypt(forge.util.decode64(data), 'RSA-OAEP', { + md: forge.md.sha512.create() + }) +} diff --git a/web/src/views/login/IndexView.vue b/web/src/views/login/IndexView.vue index 752a40bd59..92137f7a72 100644 --- a/web/src/views/login/IndexView.vue +++ b/web/src/views/login/IndexView.vue @@ -4,19 +4,24 @@ import bgImg from '@/assets/images/login_bg.webp' import { addDynamicRoutes } from '@/router' import { useThemeStore, useUserStore } from '@/store' import { getLocal, removeLocal, setLocal } from '@/utils' +import { rsaEncrypt } from '@/utils/encrypt' const router = useRouter() const route = useRoute() const query = route.query +const { data: key, loading: isLoading } = useRequest(user.key, { initialData: '' }) +const { data: isLogin } = useRequest(user.isLogin, { initialData: false }) interface LoginInfo { username: string password: string + safe_login: boolean } const loginInfo = ref({ username: '', - password: '' + password: '', + safe_login: true }) const localLoginInfo = getLocal('loginInfo') as LoginInfo @@ -36,40 +41,49 @@ async function handleLogin() { window.$message.warning('请输入用户名和密码') return } + if (!key) { + window.$message.warning('获取加密公钥失败,请刷新页面重试') + return + } try { - user.login(username, password).then(async () => { - loging.value = true - window.$notification?.success({ title: '登录成功!', duration: 2500 }) - if (isRemember.value) { - setLocal('loginInfo', { username, password }) - } else { - removeLocal('loginInfo') - } + user + .login(rsaEncrypt(username, String(unref(key))), rsaEncrypt(password, String(unref(key)))) + .then(async () => { + loging.value = true + window.$notification?.success({ title: '登录成功!', duration: 2500 }) + if (isRemember.value) { + setLocal('loginInfo', { username, password }) + } else { + removeLocal('loginInfo') + } - await addDynamicRoutes() - const { data } = await user.info() - userStore.set(data) - if (query.redirect) { - const path = query.redirect as string - Reflect.deleteProperty(query, 'redirect') - await router.push({ path, query }) - } else { - await router.push('/') - } - }) + await addDynamicRoutes() + await user.info().then((data: any) => { + userStore.set(data) + }) + if (query.redirect) { + const path = query.redirect as string + Reflect.deleteProperty(query, 'redirect') + await router.push({ path, query }) + } else { + await router.push('/') + } + }) } catch (error) { console.error(error) } loging.value = false } -onMounted(async () => { - // 已登录自动跳转 - await user.isLogin().then(async (res) => { - if (res.data) { +watch( + () => isLogin, + async () => { + if (isLogin) { + console.log(isLogin) await addDynamicRoutes() - const { data } = await user.info() - userStore.set(data) + await user.info().then((data: any) => { + userStore.set(data) + }) if (query.redirect) { const path = query.redirect as string Reflect.deleteProperty(query, 'redirect') @@ -78,8 +92,8 @@ onMounted(async () => { await router.push('/') } } - }) -}) + } +)