diff --git a/go.mod b/go.mod
index 4ef47a9..4673389 100644
--- a/go.mod
+++ b/go.mod
@@ -3,16 +3,18 @@ module next-terminal
go 1.13
require (
+ github.com/antonfisher/nested-logrus-formatter v1.3.0
github.com/gofrs/uuid v3.3.0+incompatible
github.com/gorilla/websocket v1.4.2
github.com/labstack/echo/v4 v4.1.17
github.com/labstack/gommon v0.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/sftp v1.12.0
+ github.com/sirupsen/logrus v1.2.0
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.7.1
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
gorm.io/driver/mysql v1.0.3
- gorm.io/driver/sqlite v1.1.4 // indirect
+ gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.7
)
diff --git a/go.sum b/go.sum
index c376c8a..47a23bb 100644
--- a/go.sum
+++ b/go.sum
@@ -17,6 +17,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/antonfisher/nested-logrus-formatter v1.3.0 h1:8zixYquU1Odk+vzAaAQPAdRh1ZjmUXNQ1T+dUBvlhVo=
+github.com/antonfisher/nested-logrus-formatter v1.3.0/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
diff --git a/main.go b/main.go
index 90287c2..07e0296 100644
--- a/main.go
+++ b/main.go
@@ -1,19 +1,23 @@
package main
import (
+ "bytes"
"fmt"
+ nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/labstack/gommon/log"
"github.com/patrickmn/go-cache"
+ "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
- "gorm.io/gorm/logger"
+ "io"
"next-terminal/pkg/api"
"next-terminal/pkg/config"
"next-terminal/pkg/global"
"next-terminal/pkg/handle"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
+ "os"
"strconv"
"time"
)
@@ -23,12 +27,29 @@ func main() {
}
func Run() error {
+
var err error
+ logrus.SetReportCaller(true)
+ logrus.SetFormatter(&nested.Formatter{
+ HideKeys: true,
+ FieldsOrder: []string{"component", "category"},
+ })
+
+ writer1 := &bytes.Buffer{}
+ writer2 := os.Stdout
+ writer3, err := os.OpenFile("next-terminal.log", os.O_WRONLY|os.O_CREATE, 0755)
+ if err != nil {
+ log.Fatalf("create file log.txt failed: %v", err)
+ }
+
+ logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3))
+
global.Config, err = config.SetupConfig()
if err != nil {
return err
}
+ logrus.Infof("当前数据库模式为:%v", global.Config.DB)
if global.Config.DB == "mysql" {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
global.Config.Mysql.Username,
@@ -38,13 +59,14 @@ func Run() error {
global.Config.Mysql.Database,
)
global.DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
- Logger: logger.Default.LogMode(logger.Info),
+ //Logger: logger.Default.LogMode(logger.Info),
})
} else {
global.DB, err = gorm.Open(sqlite.Open(global.Config.Sqlite.File), &gorm.Config{})
}
if err != nil {
+ logrus.Errorf("连接数据库异常:%v", err.Error())
return err
}
diff --git a/pkg/api/middleware.go b/pkg/api/middleware.go
index 748b9ab..25ad9dc 100644
--- a/pkg/api/middleware.go
+++ b/pkg/api/middleware.go
@@ -2,6 +2,7 @@ package api
import (
"github.com/labstack/echo/v4"
+ "github.com/sirupsen/logrus"
"next-terminal/pkg/global"
"strings"
"time"
@@ -35,7 +36,7 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
token := GetToken(c)
user, found := global.Cache.Get(token)
if !found {
- c.Logger().Error("您的登录信息已失效,请重新登录后再试。")
+ logrus.Debugf("您的登录信息已失效,请重新登录后再试。")
return Fail(c, 403, "您的登录信息已失效,请重新登录后再试。")
}
global.Cache.Set(token, user, time.Minute*time.Duration(30))
diff --git a/pkg/api/routes.go b/pkg/api/routes.go
index 6f9dec2..452a79b 100644
--- a/pkg/api/routes.go
+++ b/pkg/api/routes.go
@@ -19,9 +19,6 @@ func SetupRoutes() *echo.Echo {
e.File("/favicon.ico", "web/build/favicon.ico")
e.Static("/static", "web/build/static")
- // Middleware
- e.Use(middleware.Logger())
-
//fd, _ := os.OpenFile(
// "next-terminal.log",
// os.O_RDWR|os.O_APPEND,
diff --git a/pkg/api/session.go b/pkg/api/session.go
index 0580c5e..4a36e74 100644
--- a/pkg/api/session.go
+++ b/pkg/api/session.go
@@ -4,8 +4,9 @@ import (
"bytes"
"errors"
"fmt"
+ "github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
- "github.com/labstack/gommon/log"
+ "github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
@@ -16,6 +17,7 @@ import (
"path"
"strconv"
"strings"
+ "time"
)
func SessionPagingEndpoint(c echo.Context) error {
@@ -38,10 +40,10 @@ func SessionPagingEndpoint(c echo.Context) error {
recording := items[i].Recording + "/recording"
if utils.FileExists(recording) {
- log.Infof("检测到录屏文件[%v]存在", recording)
+ logrus.Debugf("检测到录屏文件[%v]存在", recording)
items[i].Recording = "1"
} else {
- log.Warnf("检测到录屏文件[%v]不存在", recording)
+ logrus.Warnf("检测到录屏文件[%v]不存在", recording)
items[i].Recording = "0"
}
} else {
@@ -89,16 +91,18 @@ func SessionDiscontentEndpoint(c echo.Context) error {
split := strings.Split(sessionIds, ",")
for i := range split {
- tun, ok := global.Store.Get(split[i])
- if ok {
- CloseSession(split[i], tun)
- }
+ CloseSessionById(split[i], 2001, "管理员强制关闭了此次接入。")
}
return Success(c, nil)
}
-func CloseSession(sessionId string, tun global.Tun) {
- _ = tun.Tun.Close()
+func CloseSessionById(sessionId string, code int, reason string) {
+ tun, _ := global.Store.Get(sessionId)
+ if tun != nil {
+ _ = tun.Tun.Close()
+ CloseSessionByWebSocket(tun.WebSocket, code, reason)
+ }
+
global.Store.Del(sessionId)
session := model.Session{}
@@ -109,6 +113,21 @@ func CloseSession(sessionId string, tun global.Tun) {
model.UpdateSessionById(&session, sessionId)
}
+func CloseSessionByWebSocket(ws *websocket.Conn, c int, t string) {
+ if ws == nil {
+ return
+ }
+ ws.SetCloseHandler(func(code int, text string) error {
+ var message []byte
+ if code != websocket.CloseNoStatusReceived {
+ message = websocket.FormatCloseMessage(c, t)
+ }
+ _ = ws.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
+ return nil
+ })
+ defer ws.Close()
+}
+
func SessionResizeEndpoint(c echo.Context) error {
width := c.QueryParam("width")
height := c.QueryParam("height")
@@ -457,6 +476,6 @@ func SessionRecordingEndpoint(c echo.Context) error {
return err
}
recording := path.Join(session.Recording, "recording")
- log.Printf("读取录屏文件:%s", recording)
+ logrus.Debugf("读取录屏文件:%s", recording)
return c.File(recording)
}
diff --git a/pkg/api/ssh.go b/pkg/api/ssh.go
index 5b2ff9f..e32cb4b 100644
--- a/pkg/api/ssh.go
+++ b/pkg/api/ssh.go
@@ -6,8 +6,8 @@ import (
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
+ "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
- "log"
"net/http"
"next-terminal/pkg/model"
"strconv"
@@ -111,7 +111,7 @@ func SSHEndpoint(c echo.Context) error {
}
_, err = stdinPipe.Write(message)
if err != nil {
- log.Println("Tunnel write:", err)
+ logrus.Debugf("Tunnel write: %v", err)
}
}
return err
@@ -173,7 +173,7 @@ func WriteMessage(ws *websocket.Conn, message string) {
func WriteByteMessage(ws *websocket.Conn, p []byte) {
err := ws.WriteMessage(websocket.TextMessage, p)
if err != nil {
- log.Println("write:", err)
+ logrus.Debugf("write: %v", err)
}
}
diff --git a/pkg/api/tunnel.go b/pkg/api/tunnel.go
index e1975a6..cf2e9ca 100644
--- a/pkg/api/tunnel.go
+++ b/pkg/api/tunnel.go
@@ -1,11 +1,9 @@
package api
import (
- "fmt"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
- "github.com/pkg/sftp"
- "log"
+ "github.com/sirupsen/logrus"
"next-terminal/pkg/global"
"next-terminal/pkg/guacd"
"next-terminal/pkg/model"
@@ -17,6 +15,7 @@ func TunEndpoint(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
+ logrus.Errorf("升级为WebSocket协议失败:%v", err.Error())
return err
}
@@ -35,7 +34,6 @@ func TunEndpoint(c echo.Context) error {
propertyMap := model.FindAllPropertiesMap()
var session model.Session
- var sftpClient *sftp.Client
if len(connectionId) > 0 {
session, err = model.FindSessionByConnectionId(connectionId)
@@ -95,11 +93,6 @@ func TunEndpoint(c echo.Context) error {
configuration.SetParameter(guacd.FontSize, strconv.Itoa(fontSize))
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
-
- sftpClient, err = CreateSftpClient(session.AssetId)
- if err != nil {
- return err
- }
break
case "vnc":
configuration.SetParameter("password", session.Password)
@@ -117,21 +110,20 @@ func TunEndpoint(c echo.Context) error {
}
addr := propertyMap[guacd.Host] + ":" + propertyMap[guacd.Port]
+
+ logrus.Infof("connect to %v with global: %+v", addr, configuration)
+
tunnel, err := guacd.NewTunnel(addr, configuration)
if err != nil {
return err
}
- fmt.Printf("=====================================================\n")
- fmt.Printf("connect to %v with global: %+v\n", addr, configuration)
- fmt.Printf("=====================================================\n")
-
tun := global.Tun{
- Tun: tunnel,
- SftpClient: sftpClient,
+ Tun: tunnel,
+ WebSocket: ws,
}
- global.Store.Set(sessionId, tun)
+ global.Store.Set(sessionId, &tun)
if len(session.ConnectionId) == 0 {
session.ConnectionId = tunnel.UUID
@@ -142,19 +134,30 @@ func TunEndpoint(c echo.Context) error {
model.UpdateSessionById(&session, sessionId)
}
+ go func() {
+ sftpClient, err := CreateSftpClient(session.AssetId)
+ if err != nil {
+ CloseSessionById(sessionId, 2002, err.Error())
+ logrus.Errorf("创建sftp客户端失败:%v", err.Error())
+ }
+ item, ok := global.Store.Get(sessionId)
+ if ok {
+ item.SftpClient = sftpClient
+ }
+ }()
+
go func() {
for true {
instruction, err := tunnel.Read()
if err != nil {
- CloseSession(sessionId, tun)
- log.Printf("WS读取异常: %v", err)
+ CloseSessionById(sessionId, 523, err.Error())
+ logrus.Printf("WebSocket读取错误: %v", err)
break
}
- //fmt.Printf("<= %v \n", string(instruction))
err = ws.WriteMessage(websocket.TextMessage, instruction)
if err != nil {
- CloseSession(sessionId, tun)
- log.Printf("WS写入异常: %v", err)
+ CloseSessionById(sessionId, 523, err.Error())
+ logrus.Printf("WebSocket写入错误: %v", err)
break
}
}
@@ -163,14 +166,14 @@ func TunEndpoint(c echo.Context) error {
for true {
_, message, err := ws.ReadMessage()
if err != nil {
- CloseSession(sessionId, tun)
- log.Printf("Tunnel读取异常: %v", err)
+ CloseSessionById(sessionId, 523, err.Error())
+ logrus.Printf("隧道读取错误: %v", err)
break
}
_, err = tunnel.WriteAndFlush(message)
if err != nil {
- CloseSession(sessionId, tun)
- log.Printf("Tunnel写入异常: %v", err)
+ CloseSessionById(sessionId, 523, err.Error())
+ logrus.Printf("隧道写入错误: %v", err)
break
}
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 701c664..311a5cc 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -1,6 +1,7 @@
package config
import (
+ "github.com/spf13/pflag"
"strings"
"github.com/spf13/viper"
@@ -39,10 +40,19 @@ func SetupConfig() (*Config, error) {
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
- err := viper.ReadInConfig()
- if err != nil {
- return nil, err
- }
+ pflag.String("db", "sqlite", "db mode")
+ pflag.String("sqlite.file", "next-terminal.db", "sqlite db file")
+ pflag.String("mysql.hostname", "127.0.0.1", "mysql hostname")
+ pflag.Int("mysql.port", 3306, "mysql port")
+ pflag.String("mysql.username", "mysql", "mysql username")
+ pflag.String("mysql.password", "mysql", "mysql password")
+ pflag.String("mysql.database", "next_terminal", "mysql database")
+
+ pflag.String("server.addr", "0.0.0.0:8088", "server listen addr")
+
+ pflag.Parse()
+ _ = viper.BindPFlags(pflag.CommandLine)
+ _ = viper.ReadInConfig()
var config = &Config{
DB: viper.GetString("db"),
diff --git a/pkg/global/store.go b/pkg/global/store.go
index 2017f93..0b1bd34 100644
--- a/pkg/global/store.go
+++ b/pkg/global/store.go
@@ -1,21 +1,23 @@
package global
import (
+ "github.com/gorilla/websocket"
"github.com/pkg/sftp"
"next-terminal/pkg/guacd"
"sync"
)
type Tun struct {
- Tun guacd.Tunnel
+ Tun *guacd.Tunnel
SftpClient *sftp.Client
+ WebSocket *websocket.Conn
}
type TunStore struct {
m sync.Map
}
-func (s *TunStore) Set(k string, v Tun) {
+func (s *TunStore) Set(k string, v *Tun) {
s.m.Store(k, v)
}
@@ -23,10 +25,10 @@ func (s *TunStore) Del(k string) {
s.m.Delete(k)
}
-func (s *TunStore) Get(k string) (item Tun, ok bool) {
+func (s *TunStore) Get(k string) (item *Tun, ok bool) {
value, ok := s.m.Load(k)
if ok {
- return value.(Tun), true
+ return value.(*Tun), true
}
return item, false
}
diff --git a/pkg/guacd/guacd.go b/pkg/guacd/guacd.go
index 1f014f9..ca9d172 100644
--- a/pkg/guacd/guacd.go
+++ b/pkg/guacd/guacd.go
@@ -106,13 +106,14 @@ type Tunnel struct {
IsOpen bool
}
-func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
+func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return
}
+ ret = &Tunnel{}
ret.conn = conn
ret.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
ret.Config = config
@@ -123,7 +124,7 @@ func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
}
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
- return Tunnel{}, err
+ return nil, err
}
args, err := ret.expect("args")
@@ -135,21 +136,21 @@ func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
height := config.GetParameter("height")
// send size
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, "96")); err != nil {
- return Tunnel{}, err
+ return nil, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("audio")); err != nil {
- return Tunnel{}, err
+ return nil, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
- return Tunnel{}, err
+ return nil, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("image")); err != nil {
- return Tunnel{}, err
+ return nil, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
- return Tunnel{}, err
+ return nil, err
}
parameters := make([]string, len(args.Args))
@@ -163,7 +164,7 @@ func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
}
// send connect
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
- return Tunnel{}, err
+ return nil, err
}
ready, err := ret.expect("ready")
@@ -172,7 +173,7 @@ func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
}
if len(ready.Args) == 0 {
- return ret, errors.New("no connection id received")
+ return nil, errors.New("no connection id received")
}
ret.UUID = ready.Args[0]
@@ -225,7 +226,6 @@ func (opt *Tunnel) ReadInstruction() (instruction Instruction, err error) {
if err != nil {
return instruction, err
}
- fmt.Printf("<- %v \n", msg)
return instruction.Parse(msg), err
}
diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js
index bcfc889..e7280ab 100644
--- a/web/src/components/asset/Asset.js
+++ b/web/src/components/asset/Asset.js
@@ -386,7 +386,7 @@ class Asset extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="资产"
>
diff --git a/web/src/components/asset/AssetModal.js b/web/src/components/asset/AssetModal.js
index 7696b6f..87588ad 100644
--- a/web/src/components/asset/AssetModal.js
+++ b/web/src/components/asset/AssetModal.js
@@ -7,12 +7,29 @@ const {Option} = Select;
// 子级页面
// Ant form create 表单内置方法
+const protocolMapping = {
+ 'ssh': [
+ {text: '自定义', value: 'custom'},
+ {text: '授权凭证', value: 'credential'},
+ {text: '私钥', value: 'private-key'}
+ ],
+ 'rdp': [{text: '自定义', value: 'custom'}, {text: '授权凭证', value: 'credential'}],
+ 'vnc': [{text: '自定义', value: 'custom'}, {text: '授权凭证', value: 'credential'}],
+ 'telnet': [{text: '自定义', value: 'custom'}, {text: '授权凭证', value: 'credential'}]
+}
+
const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoading, credentials, model}) {
const [form] = Form.useForm();
let [accountType, setAccountType] = useState(model.accountType);
+ let initAccountTypes = []
+ if (model.protocol) {
+ initAccountTypes = protocolMapping[model.protocol];
+ }
+ let [accountTypes, setAccountTypes] = useState(initAccountTypes);
+
useEffect(() => {
setAccountType(model.accountType);
});
@@ -35,18 +52,35 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
switch (e.target.value) {
case 'ssh':
port = 22;
- break;
- case 'rdp':
- port = 3389;
- break;
- case 'vnc':
- port = 5901;
+ setAccountTypes(protocolMapping['ssh']);
form.setFieldsValue({
accountType: 'custom',
});
+ handleAccountTypeChange('custom');
+ break;
+ case 'rdp':
+ port = 3389;
+ setAccountTypes(protocolMapping['rdp']);
+ form.setFieldsValue({
+ accountType: 'custom',
+ });
+ handleAccountTypeChange('custom');
+ break;
+ case 'vnc':
+ port = 5901;
+ setAccountTypes(protocolMapping['vnc']);
+ form.setFieldsValue({
+ accountType: 'custom',
+ });
+ handleAccountTypeChange('custom');
break;
case 'telnet':
port = 23;
+ setAccountTypes(protocolMapping['telnet']);
+ form.setFieldsValue({
+ accountType: 'custom',
+ });
+ handleAccountTypeChange('custom');
break;
default:
port = 65535;
@@ -57,6 +91,11 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
});
};
+ const handleAccountTypeChange = v => {
+ setAccountType(v);
+ model.accountType = v;
+ }
+
return (
-
@@ -138,13 +174,11 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
{
accountType === 'custom' ?
<>
-
+
-
+
>
diff --git a/web/src/components/command/BatchCommand.js b/web/src/components/command/BatchCommand.js
index 07c49e5..840ed1d 100644
--- a/web/src/components/command/BatchCommand.js
+++ b/web/src/components/command/BatchCommand.js
@@ -57,7 +57,7 @@ class BatchCommand extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="动态指令"
>
diff --git a/web/src/components/command/DynamicCommand.js b/web/src/components/command/DynamicCommand.js
index 605042e..de44dd8 100644
--- a/web/src/components/command/DynamicCommand.js
+++ b/web/src/components/command/DynamicCommand.js
@@ -361,7 +361,7 @@ class DynamicCommand extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="批量动态指令执行"
>
diff --git a/web/src/components/credential/Credential.js b/web/src/components/credential/Credential.js
index e75c96d..a9ab041 100644
--- a/web/src/components/credential/Credential.js
+++ b/web/src/components/credential/Credential.js
@@ -282,7 +282,7 @@ class Credential extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="访问资产的账户、密钥等"
>
diff --git a/web/src/components/dashboard/Dashboard.js b/web/src/components/dashboard/Dashboard.js
index f7d9633..585d96c 100644
--- a/web/src/components/dashboard/Dashboard.js
+++ b/web/src/components/dashboard/Dashboard.js
@@ -88,7 +88,7 @@ class Dashboard extends Component {
}}
subTitle="仪表盘"
extra={[
-
+
]}
>
diff --git a/web/src/components/session/OfflineSession.js b/web/src/components/session/OfflineSession.js
index 404d5b2..02a8724 100644
--- a/web/src/components/session/OfflineSession.js
+++ b/web/src/components/session/OfflineSession.js
@@ -359,7 +359,7 @@ class OfflineSession extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="离线会话管理"
>
diff --git a/web/src/components/session/OnlineSession.js b/web/src/components/session/OnlineSession.js
index faa387a..1d57069 100644
--- a/web/src/components/session/OnlineSession.js
+++ b/web/src/components/session/OnlineSession.js
@@ -350,7 +350,7 @@ class OnlineSession extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="查询实时在线会话"
>
diff --git a/web/src/components/setting/Setting.js b/web/src/components/setting/Setting.js
index c32c807..d962536 100644
--- a/web/src/components/setting/Setting.js
+++ b/web/src/components/setting/Setting.js
@@ -109,7 +109,7 @@ class Setting extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="系统设置"
>
diff --git a/web/src/components/user/Info.js b/web/src/components/user/Info.js
index 039f929..ffd3ac7 100644
--- a/web/src/components/user/Info.js
+++ b/web/src/components/user/Info.js
@@ -80,7 +80,7 @@ class Info extends Component {
itemRender: itemRender
}}
extra={[
-
+
]}
subTitle="个人中心"
>
diff --git a/web/src/components/user/Logout.js b/web/src/components/user/Logout.js
index a8ec264..c91c9cd 100644
--- a/web/src/components/user/Logout.js
+++ b/web/src/components/user/Logout.js
@@ -18,6 +18,7 @@ class Logout extends Component {
return (