2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 01:57:19 +08:00

feat: 实现安全登录

This commit is contained in:
耗子
2024-12-03 03:46:28 +08:00
parent 128f44ca55
commit 7bc716cff6
16 changed files with 322 additions and 105 deletions

1
go.mod
View File

@@ -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

54
go.sum
View File

@@ -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=

View File

@@ -48,7 +48,7 @@ var (
// 自动注入
var (
Version string
Version = "0.0.0"
BuildTime string
CommitHash string
GoVersion string

View File

@@ -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)
})

View File

@@ -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"`
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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",

34
web/pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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<AxiosResponse<any>> =>
request.post('/user/login', {
login: (username: string, password: string) =>
http.Post('/user/login', {
username,
password
}),
// 登出
logout: (): Promise<AxiosResponse<any>> => request.post('/user/logout'),
logout: () => http.Post('/user/logout'),
// 是否登录
isLogin: (): Promise<AxiosResponse<any>> => request.get('/user/isLogin'),
isLogin: () => http.Get('/user/isLogin'),
// 获取用户信息
info: (): Promise<AxiosResponse<any>> => request.get('/user/info')
info: () => http.Get('/user/info')
}

View File

@@ -0,0 +1 @@
export * from './rsa'

View File

@@ -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()
})
}

View File

@@ -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<LoginInfo>({
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('/')
}
}
})
})
}
)
</script>
<template>
@@ -111,16 +125,16 @@ onMounted(async () => {
</div>
<div mt-20>
<n-checkbox
:checked="isRemember"
:on-update:checked="(val: boolean) => (isRemember = val)"
label="记住我"
/>
<n-flex>
<n-checkbox v-model:checked="loginInfo.safe_login" label="安全登录" />
<n-checkbox v-model:checked="isRemember" label="记住我" />
</n-flex>
</div>
<div mt-20>
<n-button
:loading="loging"
:loading="isLoading || loging"
:disabled="isLoading || loging"
type="primary"
h-50
w-full