Merge pull request #178 from lanrenwo/user_act_log

新增用户活动日志
This commit is contained in:
bjdgyc 2022-11-08 17:04:39 +08:00 committed by GitHub
commit 5a60398435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1007 additions and 287 deletions

View File

@ -51,3 +51,29 @@ func SetAuditExport(w http.ResponseWriter, r *http.Request) {
gocsv.Marshal(datas, w)
}
func UserActLogList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var datas []dbdata.UserActLog
session := dbdata.UserActLogIns.GetSession(r.Form)
count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page)
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": dbdata.PageSize,
"datas": datas,
"statusOps": dbdata.UserActLogIns.GetStatusOpsWithTag(),
"osOps": dbdata.UserActLogIns.OsOps,
"clientOps": dbdata.UserActLogIns.ClientOps,
}
RespSucess(w, data)
}

View File

@ -178,7 +178,7 @@ func UserOnline(w http.ResponseWriter, r *http.Request) {
func UserOffline(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
token := r.FormValue("token")
sessdata.CloseSess(token)
sessdata.CloseSess(token, dbdata.UserLogoutAdmin)
RespSucess(w, nil)
}

View File

@ -45,6 +45,7 @@ func StartAdmin() {
r.HandleFunc("/set/other/audit_log/edit", SetOtherAuditLogEdit)
r.HandleFunc("/set/audit/list", SetAuditList)
r.HandleFunc("/set/audit/export", SetAuditExport)
r.HandleFunc("/set/audit/act_log_list", UserActLogList)
r.HandleFunc("/user/list", UserList)
r.HandleFunc("/user/detail", UserDetail)

View File

@ -0,0 +1,20 @@
package cron
import (
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
)
// 清除用户活动日志
func ClearUserActLog() {
lifeDay, timesUp := isClearTime()
if !timesUp {
return
}
// 当审计日志永久保存时,则退出
if lifeDay <= 0 {
return
}
affected, err := dbdata.UserActLogIns.ClearUserActLog(getTimeAgo(lifeDay))
base.Info("Cron ClearUserActLog: ", affected, err)
}

View File

@ -11,6 +11,7 @@ func Start() {
s := gocron.NewScheduler(time.Local)
s.Cron("0 * * * *").Do(ClearAudit)
s.Cron("0 * * * *").Do(ClearStatsInfo)
s.Cron("0 * * * *").Do(ClearUserActLog)
s.Every(1).Day().At("00:00").Do(sessdata.CloseUserLimittimeSession)
s.StartAsync()
}

View File

@ -14,6 +14,7 @@ type SearchCon struct {
AccessProto string `json:"access_proto"`
Date []string `json:"date"`
Info string `json:"info"`
Sort int `json:"sort"`
}
func GetAuditSession(search string) *xorm.Session {
@ -47,6 +48,11 @@ func GetAuditSession(search string) *xorm.Session {
if searchData.Info != "" {
session.And("info LIKE ?", "%"+searchData.Info+"%")
}
if searchData.Sort == 1 {
session.OrderBy("id desc")
} else {
session.OrderBy("id asc")
}
return session
}

View File

@ -33,7 +33,7 @@ func initDb() {
}
// 初始化数据库
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{})
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{}, &UserActLog{})
if err != nil {
base.Fatal(err)
}

View File

@ -40,6 +40,20 @@ type User struct {
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
}
type UserActLog struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Username string `json:"username" xorm:"varchar(60)"`
GroupName string `json:"group_name" xorm:"varchar(60)"`
IpAddr string `json:"ip_addr" xorm:"varchar(32)"`
RemoteAddr string `json:"remote_addr" xorm:"varchar(32)"`
Os uint8 `json:"os" xorm:"not null default 0 Int"`
Client uint8 `json:"client" xorm:"not null default 0 Int"`
Version string `json:"version" xorm:"varchar(15)"`
Status uint8 `json:"status" xorm:"not null default 0 Int"`
Info string `json:"info" xorm:"varchar(255) not null default ''"` // 详情
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
}
type IpMap struct {
Id int `json:"id" xorm:"pk autoincr not null"`
IpAddr string `json:"ip_addr" xorm:"varchar(32) not null unique"`

View File

@ -0,0 +1,184 @@
package dbdata
import (
"net/url"
"regexp"
"strings"
"github.com/ivpusic/grpool"
"github.com/spf13/cast"
"xorm.io/xorm"
)
const (
UserAuthFail = 0 // 认证失败
UserAuthSuccess = 1 // 认证成功
UserConnected = 2 // 连线成功
UserLogout = 3 // 用户登出
UserLogoutLose = 0 // 用户掉线
UserLogoutBanner = 1 // 用户banner弹窗取消
UserLogoutClient = 2 // 用户主动登出
UserLogoutTimeout = 3 // 用户超时登出
UserLogoutAdmin = 4 // 账号被管理员踢下线
UserLogoutExpire = 5 // 账号过期被踢下线
)
type UserActLogProcess struct {
Pool *grpool.Pool
StatusOps []string
OsOps []string
ClientOps []string
InfoOps []string
}
var (
UserActLogIns = &UserActLogProcess{
Pool: grpool.NewPool(1, 100),
StatusOps: []string{ // 操作类型
UserAuthFail: "认证失败",
UserAuthSuccess: "认证成功",
UserConnected: "连接成功",
UserLogout: "用户登出",
},
OsOps: []string{ // 操作系统
0: "Windows",
1: "macOS",
2: "Linux",
3: "Android",
4: "iOS",
5: "Unknown",
},
ClientOps: []string{ // 客户端
0: "AnyConnect",
1: "OpenConnect",
2: "Unknown",
},
InfoOps: []string{ // 信息
UserLogoutLose: "用户掉线",
UserLogoutBanner: "用户取消弹窗",
UserLogoutClient: "用户/客户端主动断开",
UserLogoutTimeout: "Session过期被踢下线",
UserLogoutAdmin: "账号被管理员踢下线",
UserLogoutExpire: "账号过期被踢下线",
},
}
)
// 异步写入用户操作日志
func (ua *UserActLogProcess) Add(u UserActLog, userAgent string) {
// os, client, ver
os_idx, client_idx, ver := ua.ParseUserAgent(userAgent)
u.Os = os_idx
u.Client = client_idx
u.Version = ver
u.RemoteAddr = strings.Split(u.RemoteAddr, ":")[0]
// remove extra characters
infoSlice := strings.Split(u.Info, " ")
infoLen := len(infoSlice)
if infoLen > 1 {
if u.Username == infoSlice[0] {
u.Info = strings.Join(infoSlice[1:], " ")
}
// delete - char
if infoLen > 2 && infoSlice[1] == "-" {
u.Info = u.Info[2:]
}
}
UserActLogIns.Pool.JobQueue <- func() {
_ = Add(u)
}
}
// 转义操作类型, 方便vue显示
func (ua *UserActLogProcess) GetStatusOpsWithTag() interface{} {
type StatusTag struct {
Key int `json:"key"`
Value string `json:"value"`
Tag string `json:"tag"`
}
var res []StatusTag
for k, v := range ua.StatusOps {
tag := "info"
switch k {
case UserAuthFail:
tag = "danger"
case UserAuthSuccess:
tag = "success"
case UserConnected:
tag = ""
}
res = append(res, StatusTag{k, v, tag})
}
return res
}
func (ua *UserActLogProcess) GetInfoOpsById(id uint8) string {
return ua.InfoOps[id]
}
func (ua *UserActLogProcess) ParseUserAgent(userAgent string) (os_idx, client_idx uint8, ver string) {
// Unknown
if len(userAgent) == 0 {
return 5, 2, ""
}
// os
os_idx = 2
if strings.Contains(userAgent, "windows") {
os_idx = 0
} else if strings.Contains(userAgent, "mac os") || strings.Contains(userAgent, "darwin_i386") {
os_idx = 1
} else if strings.Contains(userAgent, "darwin_arm") || strings.Contains(userAgent, "apple") {
os_idx = 4
} else if strings.Contains(userAgent, "android") {
os_idx = 3
}
// client
client_idx = 2
if strings.Contains(userAgent, "anyconnect") {
client_idx = 0
} else if strings.Contains(userAgent, "openconnect") {
client_idx = 1
}
// ver
uaSlice := strings.Split(userAgent, " ")
ver = uaSlice[len(uaSlice)-1]
if ver[0] == 'v' {
ver = ver[1:]
}
if !regexp.MustCompile(`^(\d+\.?)+$`).MatchString(ver) {
ver = ""
}
return
}
// 清除用户操作日志
func (ua *UserActLogProcess) ClearUserActLog(ts string) (int64, error) {
affected, err := xdb.Where("created_at < '" + ts + "'").Delete(&UserActLog{})
return affected, err
}
// 后台筛选用户操作日志
func (ua *UserActLogProcess) GetSession(values url.Values) *xorm.Session {
session := xdb.Where("1=1")
if values.Get("username") != "" {
session.And("username = ?", values.Get("username"))
}
if values.Get("sdate") != "" {
session.And("created_at >= ?", values.Get("sdate")+" 00:00:00'")
}
if values.Get("edate") != "" {
session.And("created_at <= ?", values.Get("edate")+" 23:59:59'")
}
if values.Get("status") != "" {
session.And("status = ?", cast.ToUint8(values.Get("status"))-1)
}
if values.Get("os") != "" {
session.And("os = ?", cast.ToUint8(values.Get("os"))-1)
}
if values.Get("sort") == "1" {
session.OrderBy("id desc")
} else {
session.OrderBy("id asc")
}
return session
}

View File

@ -0,0 +1,72 @@
package dbdata
import "testing"
func TestParseUserAgent(t *testing.T) {
type args struct {
userAgent string
}
type res struct {
os_idx uint8
client_idx uint8
ver string
}
tests := []struct {
name string
args args
want res
}{
{
name: "mac os 1",
args: args{userAgent: "cisco anyconnect vpn agent for mac os x 4.10.05085"},
want: res{os_idx: 1, client_idx: 0, ver: "4.10.05085"},
},
{
name: "mac os 2",
args: args{userAgent: "anyconnect darwin_i386 4.10.05085"},
want: res{os_idx: 1, client_idx: 0, ver: "4.10.05085"},
},
{
name: "windows",
args: args{userAgent: "cisco anyconnect vpn agent for windows 4.8.02042"},
want: res{os_idx: 0, client_idx: 0, ver: "4.8.02042"},
},
{
name: "iPad",
args: args{userAgent: "anyconnect applesslvpn_darwin_arm (ipad) 4.10.04060"},
want: res{os_idx: 4, client_idx: 0, ver: "4.10.04060"},
},
{
name: "iPhone",
args: args{userAgent: "cisco anyconnect vpn agent for apple iphone 4.10.04060"},
want: res{os_idx: 4, client_idx: 0, ver: "4.10.04060"},
},
{
name: "android",
args: args{userAgent: "anyconnect android 4.10.05096"},
want: res{os_idx: 3, client_idx: 0, ver: "4.10.05096"},
},
{
name: "linux",
args: args{userAgent: "open anyconnect vpn agent v7.08"},
want: res{os_idx: 2, client_idx: 0, ver: "7.08"},
},
{
name: "openconnect",
args: args{userAgent: "openconnect-gui 1.5.3 v7.08"},
want: res{os_idx: 2, client_idx: 1, ver: "7.08"},
},
{
name: "unknown",
args: args{userAgent: "unknown 1.4.3 aabcd"},
want: res{os_idx: 2, client_idx: 2, ver: ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if os_idx, client_idx, ver := UserActLogIns.ParseUserAgent(tt.args.userAgent); os_idx != tt.want.os_idx || client_idx != tt.want.client_idx || ver != tt.want.ver {
t.Errorf("ParseUserAgent() = %v, %v, %v, want %v, %v, %v", os_idx, client_idx, ver, tt.want.os_idx, tt.want.client_idx, tt.want.ver)
}
})
}
}

View File

@ -43,7 +43,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
return
}
// fmt.Printf("%+v \n", cr)
setCommonHeader(w)
if cr.Type == "logout" {
// 退出删除session信息
@ -66,16 +65,27 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
return
}
// 用户活动日志
ua := dbdata.UserActLog{
Username: cr.Auth.Username,
GroupName: cr.GroupSelect,
RemoteAddr: r.RemoteAddr,
Status: dbdata.UserAuthSuccess,
}
// TODO 用户密码校验
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
if err != nil {
base.Warn(err)
ua.Info = err.Error()
ua.Status = dbdata.UserAuthFail
dbdata.UserActLogIns.Add(ua, userAgent)
w.WriteHeader(http.StatusOK)
data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames(), Error: "用户名或密码错误"}
tplRequest(tpl_request, w, data)
return
}
dbdata.UserActLogIns.Add(ua, userAgent)
// if !ok {
// w.WriteHeader(http.StatusOK)
// data := RequestData{Group: cr.GroupSelect, Groups: base.Cfg.UserGroups, Error: "请先激活用户"}
@ -89,6 +99,8 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
sess.Group = cr.GroupSelect
sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal
sess.UserAgent = userAgent
sess.RemoteAddr = r.RemoteAddr
// 获取客户端mac地址
macHw, err := net.ParseMAC(sess.MacAddr)
if err != nil {
@ -109,7 +121,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
Banner: other.Banner, ProfileHash: profileHash}
w.WriteHeader(http.StatusOK)
tplRequest(tpl_complete, w, rd)
base.Debug("login", cr.Auth.Username)
base.Debug("login", cr.Auth.Username, userAgent)
}
const (

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/bjdgyc/anylink/sessdata"
)
@ -55,6 +56,7 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserLogoutCode = dbdata.UserLogoutClient
base.Debug("DISCONNECT", cSess.IpAddr)
return
case 0x03: // DPD-REQ

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/bjdgyc/anylink/sessdata"
)
@ -57,6 +58,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserLogoutCode = dbdata.UserLogoutClient
base.Debug("DISCONNECT DTLS", cSess.IpAddr)
return
case 0x03: // DPD-REQ

View File

@ -69,6 +69,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
cSess.SetMtu(cstpMtu)
cSess.MasterSecret = masterSecret
cSess.RemoteAddr = r.RemoteAddr
cSess.UserAgent = strings.ToLower(r.UserAgent())
cSess.LocalIp = net.ParseIP(localIp)
cstpKeepalive := base.Cfg.CstpKeepalive
cstpDpd := base.Cfg.CstpDpd
@ -194,6 +195,13 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
base.Error(err)
return
}
dbdata.UserActLogIns.Add(dbdata.UserActLog{
Username: sess.Username,
GroupName: sess.Group,
IpAddr: cSess.IpAddr.String(),
RemoteAddr: cSess.RemoteAddr,
Status: dbdata.UserConnected,
}, cSess.UserAgent)
go LinkCstp(conn, bufRW, cSess)
}

View File

@ -36,6 +36,8 @@ type ConnSession struct {
Mtu int
IfName string
Client string // 客户端 mobile pc
UserAgent string // 客户端信息
UserLogoutCode uint8 // 用户/客户端主动登出
CstpDpd int
Group *dbdata.Group
Limit *LimitRater
@ -73,6 +75,8 @@ type Session struct {
Group string
AuthStep string
AuthPass string
RemoteAddr string
UserAgent string
LastLogin time.Time
IsActive bool
@ -110,7 +114,7 @@ func checkSession() {
// 删除过期session
for _, v := range outToken {
CloseSess(v)
CloseSess(v, dbdata.UserLogoutTimeout)
}
}
}()
@ -130,7 +134,7 @@ func CloseUserLimittimeSession() {
}
sessMux.RUnlock()
for _, v := range limitTimeToken {
CloseSess(v)
CloseSess(v, dbdata.UserLogoutExpire)
}
}
@ -247,6 +251,7 @@ func (cs *ConnSession) Close() {
ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
LimitClient(cs.Username, true)
AddUserActLog(cs)
})
}
@ -407,7 +412,7 @@ func DelSess(token string) {
// sessions.Delete(token)
}
func CloseSess(token string) {
func CloseSess(token string, code ...uint8) {
sessMux.Lock()
defer sessMux.Unlock()
sess, ok := sessions[token]
@ -419,8 +424,13 @@ func CloseSess(token string) {
delete(dtlsIds, sess.DtlsSid)
if sess.CSess != nil {
if len(code) > 0 {
sess.CSess.UserLogoutCode = code[0]
}
sess.CSess.Close()
return
}
AddUserActLogBySess(sess)
}
func CloseCSess(token string) {
@ -440,5 +450,29 @@ func DelSessByStoken(stoken string) {
stoken = strings.TrimSpace(stoken)
sarr := strings.Split(stoken, "@")
token := sarr[1]
CloseSess(token)
CloseSess(token, dbdata.UserLogoutBanner)
}
func AddUserActLog(cs *ConnSession) {
ua := dbdata.UserActLog{
Username: cs.Sess.Username,
GroupName: cs.Sess.Group,
IpAddr: cs.IpAddr.String(),
RemoteAddr: cs.RemoteAddr,
Status: dbdata.UserLogout,
}
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(cs.UserLogoutCode)
dbdata.UserActLogIns.Add(ua, cs.UserAgent)
}
func AddUserActLogBySess(sess *Session) {
ua := dbdata.UserActLog{
Username: sess.Username,
GroupName: sess.Group,
IpAddr: "",
RemoteAddr: sess.RemoteAddr,
Status: dbdata.UserLogout,
}
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(1)
dbdata.UserActLogIns.Add(ua, sess.UserAgent)
}

View File

@ -0,0 +1,321 @@
<template>
<div>
<el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="search-form">
<el-form-item label="用户名:" prop="username">
<el-input size="mini" v-model="searchForm.username" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="源IP地址:" prop="src">
<el-input size="mini" v-model="searchForm.src" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的IP地址:" prop="dst">
<el-input size="mini" v-model="searchForm.dst" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的端口:" prop="dst_port">
<el-input size="mini" v-model="searchForm.dst_port" clearable style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="访问协议:">
<el-select size="mini" v-model="searchForm.access_proto" clearable placeholder="请选择" style="width: 100px">
<el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="日期范围:">
<el-date-picker
v-model="searchForm.date"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
size="mini"
align="left"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
</el-date-picker>
</el-form-item>
<el-form-item label="详情:">
<el-input size="mini" v-model="searchForm.info" placeholder="请输入关键字" clearable style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item>
<el-button
size="mini"
type="primary"
icon="el-icon-search"
@click="handleSearch">搜索
</el-button>
<el-button
size="mini"
icon="el-icon-refresh"
@click="rest">重置搜索
</el-button>
<el-button
size="mini"
icon="el-icon-download"
@click="handleExport">导出
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
v-loading="loading"
element-loading-text="玩命加载中"
element-loading-spinner="el-icon-loading"
:default-sort="{ prop: 'id', order: 'descending' }"
@sort-change="sortChange"
:header-cell-style="{backgroundColor:'#fcfcfc'}"
border>
<el-table-column
prop="id"
label="ID"
sortable="custom"
width="100">
</el-table-column>
<el-table-column
prop="username"
label="用户名"
width="140">
</el-table-column>
<el-table-column
prop="src"
label="源IP地址"
width="140">
</el-table-column>
<el-table-column
prop="dst"
label="目的IP地址"
width="140">
</el-table-column>
<el-table-column
prop="dst_port"
label="目的端口"
width="85">
</el-table-column>
<el-table-column
prop="access_proto"
label="访问协议"
width="80"
:formatter="protoFormat">
</el-table-column>
<el-table-column
prop="info"
label="详情">
</el-table-column>
<el-table-column
prop="created_at"
label="创建时间"
width="160"
:formatter="tableDateFormat">
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
:current-page.sync="currentPage"
@current-change="pageChange"
:total="count">
</el-pagination>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "auditAccess",
mixins: [],
data() {
return {
tableData: [],
count: 10,
currentPage: 1,
idSort: 1,
activeName: "first",
accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"],
defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]},
searchForm: {},
access_proto: [
{ text: 'UDP', value: '1' },
{ text: 'TCP', value: '2' },
{ text: 'HTTPS', value: '3' },
{ text: 'HTTP', value: '4' },
],
maxExportNum: 1000000,
loading: false,
rules: {
username: [
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
],
src: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
dst: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
},
}
},
watch: {
idSort: {
handler(newValue, oldValue) {
if (newValue != oldValue) {
this.getData(1);
}
},
},
},
methods: {
setSearchData() {
this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm));
},
handleSearch() {
this.$refs["searchForm"].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
this.getData(1)
})
},
searchEnterFun(e) {
var keyCode = window.event ? e.keyCode : e.which;
if (keyCode == 13) {
this.handleSearch()
}
},
getData(p) {
this.loading = true
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
}
this.searchForm.sort = this.idSort
axios.get('/set/audit/list', {
params: {
page: p,
search: this.searchForm,
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
this.loading = false
this.currentPage = p;
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
pageChange(p) {
this.getData(p)
},
handleExport() {
if (this.count > this.maxExportNum) {
var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
return s+','
})
this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出");
return ;
}
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
}
const exporting = this.$loading({
lock: true,
text: '玩命导出中,请稍等片刻...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
axios.get('/set/audit/export', {
params: {
search: this.searchForm,
}
}).then(resp => {
var rdata = resp.data
if (rdata.code && rdata.code != 0) {
exporting.close();
this.$message.error(rdata.msg);
return ;
}
exporting.close();
this.$message.success("成功导出CSV文件")
let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata
this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`)
}).catch(error => {
exporting.close();
this.$message.error('哦,请求出错');
console.log(error);
});
},
createDownLoadClick(content, fileName) {
const link = document.createElement('a')
link.href = encodeURI(content)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
protoFormat(row) {
var access_proto = row.access_proto
if (row.access_proto == 0) {
switch (row.protocol) {
case 6: access_proto = 2; break;
case 17: access_proto = 1; break;
}
}
return this.accessProtoArr[access_proto]
},
rest() {
console.log("rest");
this.setSearchData();
this.handleSearch();
},
validateIP(rule, value, callback) {
if (value === '' || typeof value === 'undefined' || value == null) {
callback()
} else {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
if ((!reg.test(value)) && value !== '') {
callback(new Error('请输入正确的IP地址'))
} else {
callback()
}
}
},
sortChange(column) {
let { order } = column;
if (order === 'ascending') {
this.idSort = 2;
} else {
this.idSort = 1;
}
},
},
}
</script>
<style scoped>
.el-form-item {
margin-bottom: 5px;
}
.el-table {
font-size: 12px;
}
.search-form >>> .el-form-item__label {
font-size: 12px;
}
/deep/ .el-table th {
padding: 5px 0;
}
</style>

View File

@ -0,0 +1,256 @@
<template>
<div>
<el-form :model="searchForm" ref="searchForm" :inline="true" class="search-form">
<el-form-item>
<el-input size="mini" v-model="searchForm.username" clearable placeholder="请输入用户名" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="searchForm.sdate"
type="date"
size="mini"
placeholder="开始日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
style="width: 130px"
>
</el-date-picker>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="searchForm.edate"
type="date"
size="mini"
placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
style="width: 130px"
>
</el-date-picker>
</el-form-item>
<el-form-item >
<el-select size="mini" v-model="searchForm.status" clearable placeholder="操作类型" style="width: 130px">
<el-option v-for="(item,index) in statusOps" :key="index" :label="item.value" :value="item.key+1">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select size="mini" v-model="searchForm.os" clearable placeholder="操作系统" style="width: 130px">
<el-option v-for="(value,item,index) in osOps" :key="index" :label="value" :value="item+1">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button
size="mini"
type="primary"
icon="el-icon-search"
@click="handleSearch">搜索
</el-button>
<el-button
size="mini"
icon="el-icon-refresh"
@click="rest">重置搜索
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
:default-sort="{ prop: 'id', order: 'descending' }"
@sort-change="sortChange"
:header-cell-style="{backgroundColor:'#fcfcfc'}"
border>
<el-table-column
prop="id"
label="ID"
sortable="custom"
width="60">
</el-table-column>
<el-table-column
prop="username"
label="用户名"
width="80">
</el-table-column>
<el-table-column
prop="group_name"
label="登陆组"
width="100">
</el-table-column>
<el-table-column
prop="status"
label="操作类型"
width="92">
<template slot-scope="{ row }">
<span v-for="(item, index) in statusOps" :key="index">
<el-tag size="small" v-if="row.status == item.key" disable-transitions :type="item.tag">{{item.value}}</el-tag>
</span>
</template>
</el-table-column>
<el-table-column
prop="os"
label="操作系统"
width="82">
<template slot-scope="{ row }">
<span v-for="(value, item, index) in osOps" :key="index">
{{ row.os == item? value: "" }}
</span>
</template>
</el-table-column>
<el-table-column
prop="client"
label="客户端"
width="100">
<template slot-scope="{ row }">
<span v-for="(value, item, index) in clientOps" :key="index">
{{ row.client == item? value: "" }}
</span>
{{ row.version }}
</template>
</el-table-column>
<el-table-column
prop="ip_addr"
label="内网IP"
width="120">
</el-table-column>
<el-table-column
prop="remote_addr"
label="外网IP"
width="120">
</el-table-column>
<el-table-column
prop="info"
label="详情">
</el-table-column>
<el-table-column
prop="created_at"
label="操作时间"
width="130"
:formatter="tableDateFormat">
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:current-page="page"
:total="count">
</el-pagination>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "List",
components: {},
mixins: [],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户信息', '登入日志'])
},
data() {
return {
page: 1,
grouNames: [],
tableData: [],
idSort: 1,
count: 10,
searchForm: {username:'', sdate:'', edate:'', status:'', os:''},
statusOps:[],
osOps:[],
clientOps:[],
}
},
watch: {
idSort: {
handler(newValue, oldValue) {
if (newValue != oldValue) {
this.getData(1);
}
},
},
},
methods: {
handleSearch() {
this.getData(1)
},
pageChange(p) {
this.getData(p)
},
searchEnterFun(e) {
var keyCode = window.event ? e.keyCode : e.which;
if (keyCode == 13) {
this.handleSearch()
}
},
getData(page) {
console.log(this.searchForm)
this.page = page
axios.get('/set/audit/act_log_list', {
params: {
page: page,
username: this.searchForm.username || '',
sdate: this.searchForm.sdate || '',
edate: this.searchForm.edate || '',
status: this.searchForm.status || '',
os: this.searchForm.os || '',
sort: this.idSort,
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
this.statusOps = data.statusOps
this.osOps = data.osOps
this.clientOps = data.clientOps
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
rest() {
console.log("rest");
this.searchForm.username = "";
this.searchForm.sdate = "";
this.searchForm.edate = "";
this.searchForm.status = "";
this.searchForm.os = "";
this.handleSearch();
},
sortChange(column) {
let { order } = column;
if (order === 'ascending') {
this.idSort = 2;
} else {
this.idSort = 1;
}
},
}
}
</script>
<style scoped>
.el-form-item {
margin-bottom: 8px;
}
.el-table {
font-size: 12px;
}
.search-form >>> .el-form-item__label {
font-size: 12px;
}
/deep/ .el-table th {
padding: 5px 0;
}
/deep/ .el-table td {
padding: 5px 0;
}
</style>

View File

@ -1,300 +1,61 @@
<template>
<div>
<el-card>
<el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="form-inner-error">
<el-form-item label="用户名:" prop="username">
<el-input size="small" v-model="searchForm.username" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="源IP地址:" prop="src">
<el-input size="small" v-model="searchForm.src" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的IP地址:" prop="dst">
<el-input size="small" v-model="searchForm.dst" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的端口:" prop="dst_port">
<el-input size="small" v-model="searchForm.dst_port" style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="访问协议:">
<el-select size="small" v-model="searchForm.access_proto" style="width: 100px">
<el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<div>
<el-form-item label="日期范围:">
<el-date-picker
v-model="searchForm.date"
type="datetimerange"
size="small"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期">
>
</el-date-picker>
</el-form-item>
<el-form-item label="详情:">
<el-input size="small" v-model="searchForm.info" placeholder="请输入关键字" style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-search"
@click="handleSearch">搜索
</el-button>
<el-button
size="small"
icon="el-icon-refresh"
@click="rest">重置搜索
</el-button>
<el-button
size="small"
icon="el-icon-download"
@click="handleExport">导出
</el-button>
</el-form-item>
</div>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
v-loading="loading"
element-loading-text="玩命加载中"
element-loading-spinner="el-icon-loading"
border>
<el-table-column
prop="id"
label="ID"
width="100">
</el-table-column>
<el-table-column
prop="username"
label="用户名"
width="140">
</el-table-column>
<el-table-column
prop="src"
label="源IP地址"
width="140">
</el-table-column>
<el-table-column
prop="dst"
label="目的IP地址"
width="140">
</el-table-column>
<el-table-column
prop="dst_port"
label="目的端口"
width="85">
</el-table-column>
<el-table-column
prop="access_proto"
label="访问协议"
width="80"
:formatter="protoFormat">
</el-table-column>
<el-table-column
prop="info"
label="详情">
</el-table-column>
<el-table-column
prop="created_at"
label="创建时间"
width="150"
:formatter="tableDateFormat">
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:total="count">
</el-pagination>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="用户访问日志" name="access_audit">
<AuditAccess ref="auditAccess"></AuditAccess>
</el-tab-pane>
<el-tab-pane label="用户活动日志" name="act_log">
<AuditActLog ref="auditActLog"></AuditActLog>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
import axios from "axios";
import AuditAccess from "../../components/audit/Access";
import AuditActLog from "../../components/audit/ActLog";
export default {
name: "Audit",
components: {},
components:{
AuditAccess,
AuditActLog
},
mixins: [],
mounted() {
this.upTab();
},
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['基础信息', '审计日志'])
},
mounted() {
this.getData(1)
this.setSearchData()
},
data() {
return {
tableData: [],
count: 10,
nowIndex: 0,
accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"],
defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]},
searchForm: {},
access_proto: [
{ text: '请选择', value: '' },
{ text: 'UDP', value: '1' },
{ text: 'TCP', value: '2' },
{ text: 'HTTPS', value: '3' },
{ text: 'HTTP', value: '4' },
],
maxExportNum: 1000000,
loading: false,
rules: {
username: [
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
],
src: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
dst: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
},
activeName: "access_audit",
}
},
methods: {
setSearchData() {
this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm));
},
handleSearch() {
this.$refs["searchForm"].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
this.getData(1)
})
},
searchEnterFun(e) {
var keyCode = window.event ? e.keyCode : e.which;
if (keyCode == 13) {
this.handleSearch()
}
},
getData(p) {
this.loading = true
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
upTab() {
var tabname = this.$route.query.tabname
if (tabname) {
this.activeName = tabname
}
axios.get('/set/audit/list', {
params: {
page: p,
search: this.searchForm,
this.handleClick(this.activeName)
},
handleClick() {
switch (this.activeName) {
case "access_audit":
this.$refs.auditAccess.setSearchData()
this.$refs.auditAccess.getData(1)
break
case "act_log":
this.$refs.auditActLog.getData(1)
break
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
this.loading = false
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
this.$router.push({path: this.$route.path, query: {tabname: this.activeName}})
},
pageChange(p) {
this.getData(p)
},
handleExport() {
if (this.count > this.maxExportNum) {
var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){
return s+','
})
this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出");
return ;
}
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
}
const exporting = this.$loading({
lock: true,
text: '玩命导出中,请稍等片刻...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
axios.get('/set/audit/export', {
params: {
search: this.searchForm,
}
}).then(resp => {
var rdata = resp.data
if (rdata.code && rdata.code != 0) {
exporting.close();
this.$message.error(rdata.msg);
return ;
}
exporting.close();
this.$message.success("成功导出CSV文件")
let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata
this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`)
}).catch(error => {
exporting.close();
this.$message.error('哦,请求出错');
console.log(error);
});
},
createDownLoadClick(content, fileName) {
const link = document.createElement('a')
link.href = encodeURI(content)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
protoFormat(row) {
var access_proto = row.access_proto
if (row.access_proto == 0) {
switch (row.protocol) {
case 6: access_proto = 2; break;
case 17: access_proto = 1; break;
}
}
return this.accessProtoArr[access_proto]
},
rest() {
console.log("rest");
this.setSearchData();
this.handleSearch();
},
validateIP(rule, value, callback) {
if (value === '' || typeof value === 'undefined' || value == null) {
callback()
} else {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
if ((!reg.test(value)) && value !== '') {
callback(new Error('请输入正确的IP地址'))
} else {
callback()
}
}
},
},
}
}
</script>
<style scoped>
</style>