新增用户活动日志

This commit is contained in:
lanrenwo 2022-11-04 15:15:58 +08:00
parent 45ed1c34f9
commit eb8d55040c
15 changed files with 947 additions and 281 deletions

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,170 @@
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 // 用户登出
)
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",
},
ClientOps: []string{ // 客户端
0: "AnyConnect",
1: "OpenConnect",
2: "unknown",
},
InfoOps: []string{ // 信息
0: "用户掉线",
1: "用户/客户端主动断开",
2: "用户被踢下线(管理员/账号过期)",
},
}
)
// 异步写入用户操作日志
func (ua *UserActLogProcess) Add(u UserActLog, userAgent string) {
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 {
infoMap := ua.InfoOps
return infoMap[id]
}
func (ua *UserActLogProcess) ParseUserAgent(userAgent string) (os_idx, client_idx uint8, ver string) {
// 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

@ -66,16 +66,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: "请先激活用户"}
@ -109,7 +120,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

@ -55,6 +55,7 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserDisconnect = true
base.Debug("DISCONNECT", cSess.IpAddr)
return
case 0x03: // DPD-REQ

View File

@ -57,6 +57,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserDisconnect = true
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,9 @@ type ConnSession struct {
Mtu int
IfName string
Client string // 客户端 mobile pc
UserAgent string // 客户端信息
UserDisconnect bool // 用户/客户端主动登出
UserKickout bool // 被踢下线
CstpDpd int
Group *dbdata.Group
Limit *LimitRater
@ -241,6 +244,7 @@ func (cs *ConnSession) Close() {
ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
LimitClient(cs.Username, true)
AddUserActLog(cs)
})
}
@ -396,6 +400,7 @@ func CloseSess(token string) {
}
delete(sessions, token)
sess.CSess.UserKickout = true
sess.CSess.Close()
}
@ -418,3 +423,22 @@ func DelSessByStoken(stoken string) {
delete(sessions, token)
sessMux.Unlock()
}
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,
}
infoId := uint8(0)
switch {
case cs.UserDisconnect:
infoId = 1
case cs.UserKickout:
infoId = 2
}
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(infoId)
dbdata.UserActLogIns.Add(ua, cs.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('/user/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-card>
<el-card>
<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()
this.$emit('update:route_name', ['基础信息', '审计日志'])
},
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 = ["", ""];
}
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
}).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 ;
methods: {
upTab() {
var tabname = this.$route.query.tabname
if (tabname) {
this.activeName = tabname
}
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);
});
this.handleClick(this.activeName)
},
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)
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
}
this.$router.push({path: this.$route.path, query: {tabname: this.activeName}})
},
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>