mirror of
https://github.com/bjdgyc/anylink.git
synced 2025-09-10 11:47:07 +08:00
@@ -2,10 +2,12 @@ package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/bjdgyc/anylink/dbdata"
|
||||
@@ -97,3 +99,257 @@ func CreatCert(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
RespSucess(w, "生成证书成功")
|
||||
}
|
||||
|
||||
// 初始化客户端 CA
|
||||
func InitClientCA(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查 CA 文件是否已存在
|
||||
caExists := true
|
||||
if _, err := os.Stat(base.Cfg.ClientCertCAFile); errors.Is(err, os.ErrNotExist) {
|
||||
caExists = false
|
||||
}
|
||||
keyExists := true
|
||||
if _, err := os.Stat(base.Cfg.ClientCertCAKeyFile); errors.Is(err, os.ErrNotExist) {
|
||||
keyExists = false
|
||||
}
|
||||
|
||||
if caExists && keyExists {
|
||||
RespError(w, RespInternalErr, "客户端 CA 已存在,请勿重复初始化,如需强制初始化可在服务器后台删除客户端CA文件")
|
||||
return
|
||||
}
|
||||
err := dbdata.GenerateClientCA()
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("客户端 CA 生成失败: %v", err))
|
||||
return
|
||||
}
|
||||
RespSucess(w, "客户端 CA 初始化成功")
|
||||
}
|
||||
|
||||
// 生成客户端证书
|
||||
func GenerateClientCert(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
if username == "" {
|
||||
RespError(w, RespInternalErr, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
groupname := r.FormValue("group_name")
|
||||
if groupname == "" {
|
||||
RespError(w, RespInternalErr, "用户组不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
user := &dbdata.User{}
|
||||
err := dbdata.One("Username", username, user)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成客户端证书
|
||||
certData, err := dbdata.GenerateClientCert(username, groupname)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("证书生成失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
RespSucess(w, certData)
|
||||
}
|
||||
|
||||
// 下载客户端 P12 证书
|
||||
func DownloadClientP12(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == "" {
|
||||
RespError(w, RespInternalErr, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// if password == "" {
|
||||
// password = "123456" // 默认密码
|
||||
// }
|
||||
|
||||
// 生成 P12 证书
|
||||
p12Data, err := dbdata.GenerateClientP12FromDB(username, password)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("证书下载失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置下载响应头
|
||||
w.Header().Set("Content-Type", "application/x-pkcs12")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.p12", username))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(p12Data)))
|
||||
w.Write(p12Data)
|
||||
}
|
||||
|
||||
// 切换客户端证书状态(禁用/启用)
|
||||
func ChangeClientCertStatus(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
if username == "" {
|
||||
RespError(w, RespInternalErr, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
clientCert, err := dbdata.GetClientCert(username)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, "证书不存在")
|
||||
return
|
||||
}
|
||||
|
||||
err = clientCert.ChangeStatus()
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("证书状态切换失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
statusText := "启用"
|
||||
if clientCert.Status == dbdata.CertStatusDisabled {
|
||||
statusText = "禁用"
|
||||
}
|
||||
|
||||
RespSucess(w, fmt.Sprintf("证书%s成功", statusText))
|
||||
}
|
||||
|
||||
// // 禁用客户端证书
|
||||
// func DisableClientCert(w http.ResponseWriter, r *http.Request) {
|
||||
// username := r.FormValue("username")
|
||||
// if username == "" {
|
||||
// RespError(w, RespInternalErr, "用户名不能为空")
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 获取证书并禁用
|
||||
// clientCert, err := dbdata.GetClientCert(username)
|
||||
// if err != nil {
|
||||
// RespError(w, RespInternalErr, "证书不存在")
|
||||
// return
|
||||
// }
|
||||
|
||||
// err = clientCert.Disable()
|
||||
// if err != nil {
|
||||
// RespError(w, RespInternalErr, fmt.Sprintf("证书禁用失败: %v", err))
|
||||
// return
|
||||
// }
|
||||
|
||||
// RespSucess(w, "证书禁用成功")
|
||||
// }
|
||||
|
||||
// 删除客户端证书
|
||||
func DeleteClientCert(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
if username == "" {
|
||||
RespError(w, RespInternalErr, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
clientCert, err := dbdata.GetClientCert(username)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, "证书不存在")
|
||||
return
|
||||
}
|
||||
|
||||
err = clientCert.Delete()
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("证书删除失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
RespSucess(w, "证书删除成功")
|
||||
}
|
||||
|
||||
// // 启用客户端证书
|
||||
// func EnableClientCert(w http.ResponseWriter, r *http.Request) {
|
||||
// username := r.FormValue("username")
|
||||
// if username == "" {
|
||||
// RespError(w, RespInternalErr, "用户名不能为空")
|
||||
// return
|
||||
// }
|
||||
|
||||
// clientCert, err := dbdata.GetClientCert(username)
|
||||
// if err != nil {
|
||||
// RespError(w, RespInternalErr, "证书不存在")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if err := clientCert.Enable(); err != nil {
|
||||
// RespError(w, RespInternalErr, fmt.Sprintf("证书启用失败: %v", err))
|
||||
// return
|
||||
// }
|
||||
|
||||
// RespSucess(w, nil)
|
||||
// }
|
||||
|
||||
// 获取客户端证书列表
|
||||
func GetClientCertList(w http.ResponseWriter, r *http.Request) {
|
||||
pageSize := 10
|
||||
pageIndex := 1
|
||||
|
||||
if r.FormValue("page_size") != "" {
|
||||
if ps, err := strconv.Atoi(r.FormValue("page_size")); err == nil {
|
||||
pageSize = ps
|
||||
}
|
||||
}
|
||||
|
||||
if r.FormValue("page_index") != "" {
|
||||
if pi, err := strconv.Atoi(r.FormValue("page_index")); err == nil {
|
||||
pageIndex = pi
|
||||
}
|
||||
}
|
||||
|
||||
certs, total, err := dbdata.GetClientCertList(pageSize, pageIndex)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, fmt.Sprintf("获取证书列表失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"list": certs,
|
||||
"total": total,
|
||||
}
|
||||
|
||||
RespSucess(w, data)
|
||||
}
|
||||
|
||||
// UserCertInfo 获取用户证书生成所需信息
|
||||
func UserCertInfo(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
|
||||
// 获取所有启用的用户
|
||||
var users []dbdata.User
|
||||
err := dbdata.Find(&users, 1000, 1)
|
||||
if err != nil && !dbdata.CheckErrNotFound(err) {
|
||||
RespError(w, RespInternalErr, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有启用的组
|
||||
var groups []dbdata.Group
|
||||
err = dbdata.Find(&groups, 1000, 1)
|
||||
if err != nil && !dbdata.CheckErrNotFound(err) {
|
||||
RespError(w, RespInternalErr, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 过滤启用的用户和组
|
||||
activeUsers := make([]dbdata.User, 0)
|
||||
for _, user := range users {
|
||||
if user.Status == 1 {
|
||||
activeUsers = append(activeUsers, user)
|
||||
}
|
||||
}
|
||||
|
||||
activeGroups := make([]dbdata.Group, 0)
|
||||
for _, group := range groups {
|
||||
if group.Status == 1 {
|
||||
activeGroups = append(activeGroups, group)
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"users": activeUsers,
|
||||
"groups": activeGroups,
|
||||
}
|
||||
|
||||
RespSucess(w, data)
|
||||
}
|
||||
|
@@ -58,6 +58,15 @@ func StartAdmin() {
|
||||
r.HandleFunc("/set/other/createcert", CreatCert)
|
||||
r.HandleFunc("/set/other/getcertset", GetCertSetting)
|
||||
r.HandleFunc("/set/other/customcert", CustomCert)
|
||||
r.HandleFunc("/set/client_cert/init_ca", InitClientCA)
|
||||
r.HandleFunc("/set/client_cert/generate", GenerateClientCert)
|
||||
r.HandleFunc("/set/client_cert/download", DownloadClientP12)
|
||||
r.HandleFunc("/set/client_cert/list", GetClientCertList)
|
||||
r.HandleFunc("/set/client_cert/changecertstatus", ChangeClientCertStatus)
|
||||
// r.HandleFunc("/set/client_cert/enable", EnableClientCert)
|
||||
// r.HandleFunc("/set/client_cert/disable", DisableClientCert)
|
||||
r.HandleFunc("/set/client_cert/delete", DeleteClientCert)
|
||||
r.HandleFunc("/set/client_cert/user_cert_info", UserCertInfo)
|
||||
|
||||
r.HandleFunc("/user/list", UserList)
|
||||
r.HandleFunc("/user/detail", UserDetail)
|
||||
|
@@ -34,29 +34,32 @@ var (
|
||||
|
||||
type ServerConfig struct {
|
||||
// LinkAddr string `json:"link_addr"`
|
||||
Conf string `json:"conf"`
|
||||
Profile string `json:"profile"`
|
||||
ProfileName string `json:"profile_name"`
|
||||
ServerAddr string `json:"server_addr"`
|
||||
ServerDTLS bool `json:"server_dtls"`
|
||||
ServerDTLSAddr string `json:"server_dtls_addr"`
|
||||
AdvertiseDTLSAddr string `json:"advertise_dtls_addr"`
|
||||
AdminAddr string `json:"admin_addr"`
|
||||
ProxyProtocol bool `json:"proxy_protocol"`
|
||||
DbType string `json:"db_type"`
|
||||
DbSource string `json:"db_source"`
|
||||
CertFile string `json:"cert_file"`
|
||||
CertKey string `json:"cert_key"`
|
||||
FilesPath string `json:"files_path"`
|
||||
LogPath string `json:"log_path"`
|
||||
LogLevel string `json:"log_level"`
|
||||
HttpServerLog bool `json:"http_server_log"`
|
||||
Pprof bool `json:"pprof"`
|
||||
Issuer string `json:"issuer"`
|
||||
AdminUser string `json:"admin_user"`
|
||||
AdminPass string `json:"admin_pass"`
|
||||
AdminOtp string `json:"admin_otp"`
|
||||
JwtSecret string `json:"jwt_secret"`
|
||||
Conf string `json:"conf"`
|
||||
Profile string `json:"profile"`
|
||||
ProfileName string `json:"profile_name"`
|
||||
ServerAddr string `json:"server_addr"`
|
||||
ServerDTLS bool `json:"server_dtls"`
|
||||
ServerDTLSAddr string `json:"server_dtls_addr"`
|
||||
AdvertiseDTLSAddr string `json:"advertise_dtls_addr"`
|
||||
AdminAddr string `json:"admin_addr"`
|
||||
ProxyProtocol bool `json:"proxy_protocol"`
|
||||
DbType string `json:"db_type"`
|
||||
DbSource string `json:"db_source"`
|
||||
CertFile string `json:"cert_file"`
|
||||
CertKey string `json:"cert_key"`
|
||||
AuthAloneCert bool `json:"auth_alone_cert"`
|
||||
ClientCertCAFile string `json:"client_ca_file"`
|
||||
ClientCertCAKeyFile string `json:"client_ca_key_file"`
|
||||
FilesPath string `json:"files_path"`
|
||||
LogPath string `json:"log_path"`
|
||||
LogLevel string `json:"log_level"`
|
||||
HttpServerLog bool `json:"http_server_log"`
|
||||
Pprof bool `json:"pprof"`
|
||||
Issuer string `json:"issuer"`
|
||||
AdminUser string `json:"admin_user"`
|
||||
AdminPass string `json:"admin_pass"`
|
||||
AdminOtp string `json:"admin_otp"`
|
||||
JwtSecret string `json:"jwt_secret"`
|
||||
|
||||
LinkMode string `json:"link_mode"` // tun tap macvtap ipvtap
|
||||
Ipv4Master string `json:"ipv4_master"` // eth0
|
||||
|
@@ -33,6 +33,9 @@ var configs = []config{
|
||||
{Typ: cfgStr, Name: "db_source", Usage: "数据库source", ValStr: "./conf/anylink.db"},
|
||||
{Typ: cfgStr, Name: "cert_file", Usage: "证书文件", ValStr: "./conf/vpn_cert.pem"},
|
||||
{Typ: cfgStr, Name: "cert_key", Usage: "证书密钥", ValStr: "./conf/vpn_cert.key"},
|
||||
{Typ: cfgBool, Name: "auth_alone_cert", Usage: "启用独立证书验证", ValBool: false},
|
||||
{Typ: cfgStr, Name: "client_ca_file", Usage: "客户端证书CA证书", ValStr: "./conf/client_ca.pem"},
|
||||
{Typ: cfgStr, Name: "client_ca_key_file", Usage: "客户端证书CA密钥", ValStr: "./conf/client_ca.key"},
|
||||
{Typ: cfgStr, Name: "files_path", Usage: "外部下载文件路径", ValStr: "./conf/files"},
|
||||
{Typ: cfgStr, Name: "log_path", Usage: "日志文件路径,默认标准输出", ValStr: ""},
|
||||
{Typ: cfgStr, Name: "log_level", Usage: "日志等级 [debug info warn error]", ValStr: "debug"},
|
||||
|
@@ -20,6 +20,7 @@
|
||||
<ExtendedKeyUsage>
|
||||
<ExtendedMatchKey>ClientAuth</ExtendedMatchKey>
|
||||
</ExtendedKeyUsage>
|
||||
<CertificateStore>User</CertificateStore>
|
||||
</CertificateMatch>
|
||||
|
||||
</ClientInitialization>
|
||||
|
@@ -9,6 +9,16 @@ db_source = "./conf/anylink.db"
|
||||
#证书文件 使用跟nginx一样的证书即可
|
||||
cert_file = "./conf/vpn_cert.pem"
|
||||
cert_key = "./conf/vpn_cert.key"
|
||||
|
||||
#是否启用独立证书验证,开启后客户端连接需要携带证书
|
||||
#如果不开启则使用用户名密码验证
|
||||
auth_alone_cert = false
|
||||
|
||||
#客户端证书CA证书
|
||||
client_cert_ca_file = "./conf/client_ca.pem"
|
||||
#客户端证书CA密钥
|
||||
client_cert_ca_key_file = "./conf/client_ca.key"
|
||||
|
||||
files_path = "./conf/files"
|
||||
profile = "./conf/profile.xml"
|
||||
#profile name(用于区分不同服务端的配置)
|
||||
@@ -52,7 +62,7 @@ admin_addr = ":8800"
|
||||
proxy_protocol = false
|
||||
|
||||
#开启go标准库http.Server的日志
|
||||
http_server_log=false
|
||||
http_server_log = false
|
||||
|
||||
#虚拟网络类型[tun macvtap tap]
|
||||
link_mode = "tun"
|
||||
|
453
server/dbdata/cert_client.go
Normal file
453
server/dbdata/cert_client.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package dbdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
// 客户端证书数据结构
|
||||
type ClientCertData struct {
|
||||
Id int `json:"id" xorm:"pk autoincr not null"`
|
||||
Username string `json:"username" xorm:"varchar(60) not null"`
|
||||
GroupName string `json:"groupname" xorm:"varchar(60)"`
|
||||
Status int `json:"status" xorm:"int default 0"`
|
||||
Certificate string `json:"certificate" xorm:"text not null"`
|
||||
PrivateKey string `json:"private_key" xorm:"text not null"`
|
||||
SerialNumber string `json:"serial_number" xorm:"varchar(100) not null"`
|
||||
NotAfter time.Time `json:"not_after" xorm:"datetime not null"`
|
||||
CreatedAt time.Time `json:"created_at" xorm:"datetime created"`
|
||||
}
|
||||
|
||||
var (
|
||||
clientCACert *x509.Certificate
|
||||
clientCAKey *rsa.PrivateKey
|
||||
caMutex sync.Mutex
|
||||
)
|
||||
|
||||
// 证书状态
|
||||
const (
|
||||
CertStatusActive = 0 // 有效
|
||||
CertStatusDisabled = 1 // 禁用
|
||||
CertStatusExpired = 2 // 过期
|
||||
)
|
||||
|
||||
// 获取证书状态描述
|
||||
func (c *ClientCertData) GetStatusText() string {
|
||||
switch c.GetStatus() {
|
||||
case CertStatusActive:
|
||||
return "有效"
|
||||
case CertStatusDisabled:
|
||||
return "禁用"
|
||||
case CertStatusExpired:
|
||||
return "过期"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// 获取证书状态
|
||||
func (c *ClientCertData) GetStatus() int {
|
||||
return c.Status
|
||||
}
|
||||
|
||||
// 保存客户端证书
|
||||
func (c *ClientCertData) Save() error {
|
||||
if c.Id > 0 {
|
||||
return Set(c) // 更新现有记录
|
||||
}
|
||||
return Add(c)
|
||||
}
|
||||
|
||||
// 禁用证书
|
||||
func (c *ClientCertData) Disable() error {
|
||||
return c.UpdateStatus(CertStatusDisabled)
|
||||
}
|
||||
|
||||
// 启用证书
|
||||
func (c *ClientCertData) Enable() error {
|
||||
return c.UpdateStatus(CertStatusActive)
|
||||
}
|
||||
|
||||
// 删除证书记录
|
||||
func (c *ClientCertData) Delete() error {
|
||||
return Del(c)
|
||||
}
|
||||
|
||||
// 切换证书状态
|
||||
func (c *ClientCertData) ChangeStatus() error {
|
||||
switch c.Status {
|
||||
case CertStatusActive:
|
||||
return c.Disable()
|
||||
case CertStatusDisabled:
|
||||
return c.Enable()
|
||||
}
|
||||
return fmt.Errorf("证书已过期,无法切换状态")
|
||||
}
|
||||
|
||||
// 更新客户端证书状态
|
||||
func (c *ClientCertData) UpdateStatus(status int) error {
|
||||
c.Status = status
|
||||
if err := Set(c); err != nil {
|
||||
return fmt.Errorf("更新客户端证书状态失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查并更新证书状态为过期
|
||||
func (c *ClientCertData) CheckAndUpdateStatus() error {
|
||||
if c.Status != CertStatusExpired && time.Now().After(c.NotAfter) {
|
||||
if err := c.UpdateStatus(CertStatusExpired); err != nil {
|
||||
return fmt.Errorf("更新证书状态为过期失败: %v", err)
|
||||
}
|
||||
base.Info("检测到证书过期,已更新状态:", c.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取客户端证书列表
|
||||
func GetClientCertList(pageSize int, pageIndex int) ([]ClientCertData, int64, error) {
|
||||
var certs []ClientCertData
|
||||
session := GetXdb().NewSession()
|
||||
defer session.Close()
|
||||
total, err := FindAndCount(session, &certs, pageSize, pageIndex)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取客户端证书列表失败: %v", err)
|
||||
}
|
||||
return certs, total, nil
|
||||
}
|
||||
|
||||
// 获取客户端证书
|
||||
func GetClientCert(username string) (*ClientCertData, error) {
|
||||
clientCert := &ClientCertData{
|
||||
Username: username,
|
||||
}
|
||||
err := One("Username", username, clientCert)
|
||||
return clientCert, err
|
||||
}
|
||||
|
||||
// 生成客户端 CA 证书
|
||||
func GenerateClientCA() error {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "AnyLink Client CA"},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), // 10年有效期
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入 CA 证书文件
|
||||
certOut, err := os.OpenFile(base.Cfg.ClientCertCAFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer certOut.Close()
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
|
||||
// 写入 CA 私钥文件
|
||||
keyOut, err := os.OpenFile(base.Cfg.ClientCertCAKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
}
|
||||
|
||||
// 生成客户端证书并保存到数据库
|
||||
func GenerateClientCert(username, groupname string) (*ClientCertData, error) {
|
||||
// 检查用户是否存在并验证组成员资格
|
||||
user := &User{}
|
||||
err := One("Username", username, user)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("用户不存在: %s", username)
|
||||
}
|
||||
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查用户是否属于指定组
|
||||
if !slices.Contains(user.Groups, groupname) {
|
||||
return nil, fmt.Errorf("用户 %s 不属于组 %s", username, groupname)
|
||||
}
|
||||
// 检查是否已存在证书记录
|
||||
_, err = GetClientCert(username)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("获取用户证书失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
// 用户已有证书记录,不允许重复生成
|
||||
return nil, fmt.Errorf("用户 %s 已存在证书,请先删除现有证书", username)
|
||||
}
|
||||
|
||||
// 确保客户端 CA 已加载
|
||||
if err := LoadClientCA(); err != nil {
|
||||
return nil, fmt.Errorf("无法加载客户端 CA: %v", err)
|
||||
}
|
||||
|
||||
// 生成客户端私钥
|
||||
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建客户端证书模板
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: username,
|
||||
OrganizationalUnit: []string{groupname},
|
||||
Organization: []string{"AnyLink VPN"},
|
||||
Country: []string{"CN"},
|
||||
Province: []string{"Beijing"},
|
||||
Locality: []string{"Beijing"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365), // 1年有效期
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
DNSNames: []string{username},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
}
|
||||
|
||||
// 签发客户端证书
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, clientCACert, &clientKey.PublicKey, clientCAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 编码证书和私钥为 PEM 格式
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey)})
|
||||
|
||||
// 保存到数据库
|
||||
clientCertData := &ClientCertData{
|
||||
Username: username,
|
||||
GroupName: groupname,
|
||||
Certificate: string(certPEM),
|
||||
PrivateKey: string(keyPEM),
|
||||
SerialNumber: template.SerialNumber.String(),
|
||||
NotAfter: template.NotAfter,
|
||||
CreatedAt: time.Now(),
|
||||
Status: CertStatusActive, // 初始状态为有效
|
||||
}
|
||||
|
||||
if err := clientCertData.Save(); err != nil {
|
||||
return nil, fmt.Errorf("保存客户端证书失败: %v", err)
|
||||
}
|
||||
|
||||
return clientCertData, nil
|
||||
}
|
||||
|
||||
// 生成 PKCS#12 格式证书文件
|
||||
func GenerateClientP12FromDB(username string, password string) ([]byte, error) {
|
||||
// 从数据库获取证书
|
||||
clientCert, err := GetClientCert(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查并更新证书状态
|
||||
if err := clientCert.CheckAndUpdateStatus(); err != nil {
|
||||
base.Error("检查并更新证书状态失败:", err)
|
||||
}
|
||||
// 检查证书状态
|
||||
if clientCert.GetStatus() != CertStatusActive {
|
||||
return nil, fmt.Errorf("用户 %s 的证书状态为:%s", username, clientCert.GetStatusText())
|
||||
}
|
||||
|
||||
// 确保客户端 CA 已加载
|
||||
if err := LoadClientCA(); err != nil {
|
||||
return nil, fmt.Errorf("无法加载客户端 CA: %v", err)
|
||||
}
|
||||
|
||||
// 解析证书和私钥
|
||||
certBlock, _ := pem.Decode([]byte(clientCert.Certificate))
|
||||
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBlock, _ := pem.Decode([]byte(clientCert.PrivateKey))
|
||||
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 打包为 .p12 格式
|
||||
p12Data, err := pkcs12.Modern.Encode(key, cert, []*x509.Certificate{clientCACert}, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p12Data, nil
|
||||
}
|
||||
|
||||
// 验证客户端证书
|
||||
func ValidateClientCert(cert *x509.Certificate, userAgent string) bool {
|
||||
// 获取用户和证书信息
|
||||
user := &User{
|
||||
Username: cert.Subject.CommonName,
|
||||
}
|
||||
err := One("Username", user.Username, user)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
base.Error("证书验证失败:用户不存在", cert.Subject.CommonName)
|
||||
} else {
|
||||
base.Error("证书验证失败:查询用户失败:", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查用户状态是否启用
|
||||
if user.Status != 1 {
|
||||
base.Error("证书验证失败:用户已禁用:", user.Username)
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取客户端证书记录
|
||||
clientCertData, err := GetClientCert(user.Username)
|
||||
if err != nil {
|
||||
base.Error("证书验证失败:获取客户端证书失败:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if clientCertData.GroupName != cert.Subject.OrganizationalUnit[0] {
|
||||
base.Error("证书验证失败:证书组名与用户组名不匹配")
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查证书状态
|
||||
if clientCertData.GetStatus() != CertStatusActive {
|
||||
base.Error("证书验证失败:", user.Username, "证书状态为", clientCertData.GetStatusText())
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查证书是否过期
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
base.Error("证书验证失败:证书已过期:", cert.NotAfter)
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证证书指纹
|
||||
storedCertBlock, _ := pem.Decode([]byte(clientCertData.Certificate))
|
||||
storedCert, err := x509.ParseCertificate(storedCertBlock.Bytes)
|
||||
if err != nil {
|
||||
base.Error("证书验证失败:解析存储证书失败:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 比较证书的完整内容
|
||||
if !bytes.Equal(cert.Raw, storedCert.Raw) {
|
||||
base.Error("证书验证失败:证书内容不匹配")
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证证书链
|
||||
verifyOptions := x509.VerifyOptions{
|
||||
Roots: LoadClientCAPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
if _, err := cert.Verify(verifyOptions); err != nil {
|
||||
base.Error("证书验证失败:证书链验证失败:", err)
|
||||
return false
|
||||
}
|
||||
// 检查扩展密钥用途
|
||||
hasClientAuth := slices.Contains(cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
|
||||
if !hasClientAuth {
|
||||
base.Error("证书验证失败:证书缺少客户端认证扩展")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 加载客户端 CA 证书池
|
||||
func LoadClientCAPool() *x509.CertPool {
|
||||
if err := LoadClientCA(); err != nil {
|
||||
return nil
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(clientCACert)
|
||||
return caCertPool
|
||||
}
|
||||
|
||||
// 加载客户端 CA 证书和私钥
|
||||
func LoadClientCA() error {
|
||||
caMutex.Lock()
|
||||
defer caMutex.Unlock()
|
||||
|
||||
// 如果证书已经加载到内存中,则直接返回
|
||||
if clientCACert != nil && clientCAKey != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
caCertPEM, readErr := os.ReadFile(base.Cfg.ClientCertCAFile)
|
||||
if readErr != nil {
|
||||
base.Warn("无法读取客户端 CA 证书,请初始化CA:", readErr)
|
||||
return fmt.Errorf("无法读取客户端 CA 证书,请初始化CA: %w", readErr)
|
||||
}
|
||||
caKeyPEM, readErr := os.ReadFile(base.Cfg.ClientCertCAKeyFile)
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("无法读取客户端 CA 私钥: %w", readErr)
|
||||
}
|
||||
|
||||
caCertBlock, _ := pem.Decode(caCertPEM)
|
||||
if caCertBlock == nil {
|
||||
return errors.New("无法解析客户端 CA 证书 PEM 块")
|
||||
}
|
||||
|
||||
var parseErr error
|
||||
clientCACert, parseErr = x509.ParseCertificate(caCertBlock.Bytes)
|
||||
if parseErr != nil {
|
||||
return fmt.Errorf("无法解析客户端 CA 证书: %w", parseErr)
|
||||
}
|
||||
|
||||
caKeyBlock, _ := pem.Decode(caKeyPEM)
|
||||
if caKeyBlock == nil {
|
||||
return errors.New("无法解析客户端 CA 私钥 PEM 块")
|
||||
}
|
||||
|
||||
var parseKeyErr error
|
||||
clientCAKey, parseKeyErr = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
|
||||
if parseKeyErr != nil {
|
||||
// 解析为PKCS8
|
||||
pkcs8Key, pkcs8Err := x509.ParsePKCS8PrivateKey(caKeyBlock.Bytes)
|
||||
if pkcs8Err != nil {
|
||||
return fmt.Errorf("无法解析客户端 CA 私钥 (PKCS1 or PKCS8): %w", parseKeyErr)
|
||||
}
|
||||
var ok bool
|
||||
clientCAKey, ok = pkcs8Key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return errors.New("解析私钥成功,但不是 RSA 类型")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
194
server/dbdata/cert_client_test.go
Normal file
194
server/dbdata/cert_client_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package dbdata
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateClientCert(t *testing.T) {
|
||||
base.Test()
|
||||
ast := assert.New(t)
|
||||
|
||||
// 设置临时目录用于测试
|
||||
tempDir := t.TempDir()
|
||||
base.Cfg.ClientCertCAFile = tempDir + "/client_ca.pem"
|
||||
base.Cfg.ClientCertCAKeyFile = tempDir + "/client_ca_key.pem"
|
||||
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
// 使用 GenerateClientCA 生成 CA
|
||||
err := GenerateClientCA()
|
||||
ast.Nil(err, "生成客户端 CA 失败")
|
||||
|
||||
// 创建测试组
|
||||
group := "cert-test-group"
|
||||
dns := []ValData{{Val: "8.8.8.8"}}
|
||||
g := Group{Name: group, Status: 1, ClientDns: dns}
|
||||
err = SetGroup(&g)
|
||||
ast.Nil(err)
|
||||
|
||||
// 创建测试用户
|
||||
username := "cert-test-user"
|
||||
u := User{Username: username, Groups: []string{group}, Status: 1}
|
||||
err = SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 测试证书生成成功
|
||||
certData, err := GenerateClientCert(username, group)
|
||||
ast.Nil(err)
|
||||
ast.NotNil(certData)
|
||||
ast.Equal(username, certData.Username)
|
||||
ast.Equal(group, certData.GroupName)
|
||||
ast.Equal(CertStatusActive, certData.Status)
|
||||
ast.NotEmpty(certData.Certificate)
|
||||
ast.NotEmpty(certData.PrivateKey)
|
||||
ast.NotEmpty(certData.SerialNumber)
|
||||
|
||||
// 测试重复生成证书失败
|
||||
_, err = GenerateClientCert(username, group)
|
||||
ast.NotNil(err)
|
||||
ast.Contains(err.Error(), "已存在证书")
|
||||
|
||||
// 测试用户不属于指定组
|
||||
_, err = GenerateClientCert(username, "nonexistent-group")
|
||||
ast.NotNil(err)
|
||||
ast.Contains(err.Error(), "不属于组")
|
||||
|
||||
// 测试用户不存在
|
||||
_, err = GenerateClientCert("nonexistent-user", group)
|
||||
ast.NotNil(err)
|
||||
ast.Contains(err.Error(), "用户不存在")
|
||||
}
|
||||
func TestCertificateAuthFlow(t *testing.T) {
|
||||
base.Test()
|
||||
ast := assert.New(t)
|
||||
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
// 设置测试环境
|
||||
group := "auth-test-group"
|
||||
username := "auth-test-user"
|
||||
|
||||
// 创建组和用户
|
||||
dns := []ValData{{Val: "8.8.8.8"}}
|
||||
g := Group{Name: group, Status: 1, ClientDns: dns}
|
||||
err := SetGroup(&g)
|
||||
ast.Nil(err)
|
||||
|
||||
u := User{Username: username, Groups: []string{group}, Status: 1}
|
||||
err = SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 生成证书
|
||||
certData, err := GenerateClientCert(username, group)
|
||||
ast.Nil(err)
|
||||
|
||||
// 解析证书
|
||||
cert, err := parseCertFromPEM(certData.Certificate)
|
||||
ast.Nil(err)
|
||||
|
||||
// 证书验证
|
||||
valid := ValidateClientCert(cert, "test-agent")
|
||||
ast.True(valid)
|
||||
|
||||
// 测试证书状态变更
|
||||
certData.Status = CertStatusDisabled
|
||||
err = certData.UpdateStatus(CertStatusDisabled)
|
||||
ast.Nil(err)
|
||||
|
||||
valid = ValidateClientCert(cert, "test-agent")
|
||||
ast.False(valid)
|
||||
}
|
||||
|
||||
func TestValidateClientCert(t *testing.T) {
|
||||
base.Test()
|
||||
ast := assert.New(t)
|
||||
|
||||
// 设置临时目录用于测试
|
||||
tempDir := t.TempDir()
|
||||
base.Cfg.ClientCertCAFile = tempDir + "/client_ca.pem"
|
||||
base.Cfg.ClientCertCAKeyFile = tempDir + "/client_ca_key.pem"
|
||||
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
// 初始化客户端 CA
|
||||
err := GenerateClientCA()
|
||||
ast.Nil(err, "初始化客户端 CA 失败")
|
||||
|
||||
// 创建测试组
|
||||
group := "test-group"
|
||||
dns := []ValData{{Val: "8.8.8.8"}}
|
||||
g := Group{Name: group, Status: 1, ClientDns: dns}
|
||||
err = SetGroup(&g)
|
||||
ast.Nil(err)
|
||||
|
||||
// 创建测试用户
|
||||
username := "test-user"
|
||||
u := User{Username: username, Groups: []string{group}, Status: 1}
|
||||
err = SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 生成客户端证书
|
||||
certData, err := GenerateClientCert(username, group)
|
||||
ast.Nil(err)
|
||||
ast.NotNil(certData)
|
||||
ast.Equal(username, certData.Username)
|
||||
ast.Equal(group, certData.GroupName)
|
||||
|
||||
// 解析生成的证书
|
||||
cert, err := parseCertFromPEM(certData.Certificate)
|
||||
ast.Nil(err)
|
||||
ast.Equal(username, cert.Subject.CommonName)
|
||||
ast.Equal(group, cert.Subject.OrganizationalUnit[0])
|
||||
|
||||
// 测试证书验证成功
|
||||
valid := ValidateClientCert(cert, "test-agent")
|
||||
ast.True(valid)
|
||||
|
||||
// 测试用户不存在的情况
|
||||
cert.Subject.CommonName = "nonexistent-user"
|
||||
valid = ValidateClientCert(cert, "test-agent")
|
||||
ast.False(valid)
|
||||
|
||||
// 测试用户被禁用的情况
|
||||
cert.Subject.CommonName = username
|
||||
u.Status = 0
|
||||
err = SetUser(&u)
|
||||
ast.Nil(err)
|
||||
valid = ValidateClientCert(cert, "test-agent")
|
||||
ast.False(valid)
|
||||
|
||||
// 恢复用户状态
|
||||
u.Status = 1
|
||||
err = SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 测试证书组不匹配的情况
|
||||
cert.Subject.OrganizationalUnit[0] = "wrong-group"
|
||||
valid = ValidateClientCert(cert, "test-agent")
|
||||
ast.False(valid)
|
||||
|
||||
// 测试证书状态被禁用的情况
|
||||
cert.Subject.OrganizationalUnit[0] = group
|
||||
certData.Status = CertStatusDisabled
|
||||
err = certData.Save()
|
||||
ast.Nil(err)
|
||||
valid = ValidateClientCert(cert, "test-agent")
|
||||
ast.False(valid)
|
||||
}
|
||||
|
||||
func parseCertFromPEM(certPEM string) (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode([]byte(certPEM))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode PEM block")
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
@@ -36,7 +36,7 @@ func initDb() {
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{}, &UserActLog{}, &PasswordReset{})
|
||||
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{}, &UserActLog{}, &PasswordReset{}, &ClientCertData{})
|
||||
if err != nil {
|
||||
base.Fatal(err)
|
||||
}
|
||||
|
@@ -120,7 +120,8 @@ type StatsMem struct {
|
||||
}
|
||||
|
||||
type PasswordReset struct {
|
||||
Token string `json:"token" xorm:"varchar(60) not null unique"`
|
||||
UserId int `json:"id" xorm:"not null"`
|
||||
ExpiresAt int `json:"expires_at" xorm:"not null"`
|
||||
Token string `json:"token" xorm:"varchar(60) not null unique"`
|
||||
UserId int `json:"id" xorm:"not null"`
|
||||
ExpiresAt int `json:"expires_at" xorm:"not null"`
|
||||
LastRequestTime int `json:"last_request_time" xorm:"int default 0"`
|
||||
}
|
||||
|
@@ -114,5 +114,6 @@ require (
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.6.0
|
||||
xorm.io/builder v0.3.13 // indirect
|
||||
)
|
||||
|
@@ -418,6 +418,8 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
|
||||
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
|
||||
|
@@ -55,6 +55,19 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
base.Trace(fmt.Sprintf("%+v \n", cr))
|
||||
// 用户活动日志
|
||||
ua := &dbdata.UserActLog{
|
||||
Username: cr.Auth.Username,
|
||||
GroupName: cr.GroupSelect,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
DeviceType: cr.DeviceId.DeviceType,
|
||||
PlatformVersion: cr.DeviceId.PlatformVersion,
|
||||
}
|
||||
sessionData := &AuthSession{
|
||||
ClientRequest: cr,
|
||||
UserActLog: ua,
|
||||
}
|
||||
// setCommonHeader(w)
|
||||
if cr.Type == "logout" {
|
||||
// 退出删除session信息
|
||||
@@ -64,6 +77,44 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
// 检查客户端证书认证
|
||||
if base.Cfg.AuthAloneCert {
|
||||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
clientCert := r.TLS.PeerCertificates[0]
|
||||
username := clientCert.Subject.CommonName
|
||||
groupname := clientCert.Subject.OrganizationalUnit[0]
|
||||
if username == "" || groupname == "" {
|
||||
base.Warn("客户端证书缺少用户名或组名")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ua.Username = username
|
||||
ua.GroupName = groupname
|
||||
// 验证证书有效性和用户状态
|
||||
if dbdata.ValidateClientCert(clientCert, userAgent) {
|
||||
// 证书认证成功,创建会话
|
||||
base.Info("用户通过证书认证:", username)
|
||||
|
||||
sessionData.ClientRequest.GroupSelect = groupname
|
||||
sessionData.ClientRequest.Auth.Username = username
|
||||
ua.Info = "用户通过证书认证登录"
|
||||
ua.Status = dbdata.UserConnected
|
||||
dbdata.UserActLogIns.Add(*ua, userAgent)
|
||||
|
||||
CreateSession(w, r, sessionData)
|
||||
return
|
||||
} else {
|
||||
ua.Info = "客户端证书验证失败"
|
||||
ua.Status = dbdata.UserAuthFail
|
||||
dbdata.UserActLogIns.Add(*ua, userAgent)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
base.Warn("启用了独立证书验证,但用户未提供有效证书")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if cr.Type == "init" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -84,22 +135,8 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 用户活动日志
|
||||
ua := &dbdata.UserActLog{
|
||||
Username: cr.Auth.Username,
|
||||
GroupName: cr.GroupSelect,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
DeviceType: cr.DeviceId.DeviceType,
|
||||
PlatformVersion: cr.DeviceId.PlatformVersion,
|
||||
}
|
||||
|
||||
sessionData := &AuthSession{
|
||||
ClientRequest: cr,
|
||||
UserActLog: ua,
|
||||
}
|
||||
// TODO 用户密码校验
|
||||
ext := map[string]interface{}{"mac_addr": cr.MacAddressList.MacAddress}
|
||||
ext := map[string]any{"mac_addr": cr.MacAddressList.MacAddress}
|
||||
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect, ext)
|
||||
if err != nil {
|
||||
lockManager.UpdateLoginStatus(cr.Auth.Username, r.RemoteAddr, false) // 记录登录失败状态
|
||||
|
339
server/handler/link_auth_otp_test.go
Normal file
339
server/handler/link_auth_otp_test.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/bjdgyc/anylink/dbdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xlzd/gotp"
|
||||
)
|
||||
|
||||
func TestSessionStore(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
// 测试会话存储基本功能
|
||||
store := NewSessionStore()
|
||||
sessionID := "test-session-123"
|
||||
|
||||
// 创建测试会话数据
|
||||
authSession := &AuthSession{
|
||||
ClientRequest: &ClientRequest{
|
||||
Auth: auth{
|
||||
Username: "test-user",
|
||||
OtpSecret: "JBSWY3DPEHPK3PXP",
|
||||
},
|
||||
GroupSelect: "test-group",
|
||||
},
|
||||
UserActLog: &dbdata.UserActLog{
|
||||
Username: "test-user",
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
// 测试保存会话
|
||||
store.SaveAuthSession(sessionID, authSession)
|
||||
|
||||
// 测试获取会话
|
||||
retrievedSession, err := store.GetAuthSession(sessionID)
|
||||
ast.Nil(err)
|
||||
ast.NotNil(retrievedSession)
|
||||
ast.Equal("test-user", retrievedSession.ClientRequest.Auth.Username)
|
||||
|
||||
// 测试获取不存在的会话
|
||||
_, err = store.GetAuthSession("nonexistent-session")
|
||||
ast.NotNil(err)
|
||||
ast.Contains(err.Error(), "auth session not found")
|
||||
|
||||
// 测试删除会话
|
||||
store.DeleteAuthSession(sessionID)
|
||||
_, err = store.GetAuthSession(sessionID)
|
||||
ast.NotNil(err)
|
||||
}
|
||||
|
||||
func TestGenerateSessionID(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
// 测试会话ID生成
|
||||
sessionID, err := GenerateSessionID()
|
||||
ast.Nil(err)
|
||||
ast.NotEmpty(sessionID)
|
||||
ast.Equal(32, len(sessionID))
|
||||
|
||||
// 测试生成的ID唯一性
|
||||
sessionID2, err := GenerateSessionID()
|
||||
ast.Nil(err)
|
||||
ast.NotEqual(sessionID, sessionID2)
|
||||
}
|
||||
|
||||
func TestCookieOperations(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
// 测试设置和获取Cookie
|
||||
w := httptest.NewRecorder()
|
||||
SetCookie(w, "test-cookie", "test-value", 3600)
|
||||
|
||||
cookies := w.Result().Cookies()
|
||||
ast.Equal(1, len(cookies))
|
||||
ast.Equal("test-cookie", cookies[0].Name)
|
||||
ast.Equal("test-value", cookies[0].Value)
|
||||
ast.True(cookies[0].HttpOnly)
|
||||
ast.True(cookies[0].Secure)
|
||||
|
||||
// 测试从请求中获取Cookie
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.AddCookie(cookies[0])
|
||||
|
||||
value, err := GetCookie(req, "test-cookie")
|
||||
ast.Nil(err)
|
||||
ast.Equal("test-value", value)
|
||||
|
||||
// 测试获取不存在的Cookie
|
||||
_, err = GetCookie(req, "nonexistent-cookie")
|
||||
ast.NotNil(err)
|
||||
|
||||
// 测试删除Cookie
|
||||
w2 := httptest.NewRecorder()
|
||||
DeleteCookie(w2, "test-cookie")
|
||||
deleteCookies := w2.Result().Cookies()
|
||||
ast.Equal(1, len(deleteCookies))
|
||||
ast.Equal("test-cookie", deleteCookies[0].Name)
|
||||
ast.Equal("", deleteCookies[0].Value)
|
||||
ast.Equal(-1, deleteCookies[0].MaxAge)
|
||||
}
|
||||
|
||||
func TestLinkAuthOtp(t *testing.T) {
|
||||
base.Test()
|
||||
ast := assert.New(t)
|
||||
|
||||
base.Cfg.DisplayError = true
|
||||
|
||||
// 设置测试数据库
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
// 创建测试组
|
||||
group := "otp-test-group"
|
||||
dns := []dbdata.ValData{{Val: "8.8.8.8"}}
|
||||
g := dbdata.Group{Name: group, Status: 1, ClientDns: dns}
|
||||
err := dbdata.SetGroup(&g)
|
||||
ast.Nil(err)
|
||||
|
||||
// 创建测试用户
|
||||
username := "otp-test-user"
|
||||
otpSecret := "JBSWY3DPEHPK3PXP"
|
||||
u := dbdata.User{
|
||||
Username: username,
|
||||
Groups: []string{group},
|
||||
Status: 1,
|
||||
OtpSecret: otpSecret,
|
||||
}
|
||||
err = dbdata.SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 生成有效的OTP代码
|
||||
totp := gotp.NewDefaultTOTP(otpSecret)
|
||||
validOtp := totp.Now()
|
||||
|
||||
// 创建测试会话
|
||||
sessionID := "test-otp-session"
|
||||
authSession := &AuthSession{
|
||||
ClientRequest: &ClientRequest{
|
||||
Auth: auth{
|
||||
Username: username,
|
||||
OtpSecret: otpSecret,
|
||||
},
|
||||
GroupSelect: group,
|
||||
UserAgent: "test-agent",
|
||||
},
|
||||
UserActLog: &dbdata.UserActLog{
|
||||
Username: username,
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
},
|
||||
}
|
||||
SessStore.SaveAuthSession(sessionID, authSession)
|
||||
|
||||
// 测试成功的OTP验证
|
||||
t.Run("ValidOTP", func(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
// 创建OTP验证请求
|
||||
clientReq := ClientRequest{
|
||||
Auth: auth{
|
||||
SecondaryPassword: validOtp,
|
||||
},
|
||||
}
|
||||
reqBody, _ := xml.Marshal(clientReq)
|
||||
|
||||
req := httptest.NewRequest("POST", "/otp-verification", bytes.NewReader(reqBody))
|
||||
req.AddCookie(&http.Cookie{Name: "auth-session-id", Value: sessionID})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
LinkAuth_otp(w, req)
|
||||
|
||||
ast.Equal(http.StatusOK, w.Code)
|
||||
// 验证会话已被删除
|
||||
_, err := SessStore.GetAuthSession(sessionID)
|
||||
ast.NotNil(err)
|
||||
})
|
||||
|
||||
// 测试无效的OTP代码
|
||||
t.Run("InvalidOTP", func(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
// 重新创建会话(因为上一个测试中被删除了)
|
||||
SessStore.SaveAuthSession(sessionID+"2", authSession)
|
||||
|
||||
clientReq := ClientRequest{
|
||||
Auth: auth{
|
||||
SecondaryPassword: "123456", // 无效的OTP
|
||||
},
|
||||
}
|
||||
reqBody, _ := xml.Marshal(clientReq)
|
||||
|
||||
req := httptest.NewRequest("POST", "/otp-verification", bytes.NewReader(reqBody))
|
||||
req.AddCookie(&http.Cookie{Name: "auth-session-id", Value: sessionID + "2"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
LinkAuth_otp(w, req)
|
||||
|
||||
ast.Equal(http.StatusOK, w.Code)
|
||||
// 验证响应包含错误信息
|
||||
ast.Contains(w.Body.String(), "OTP 动态码错误")
|
||||
})
|
||||
|
||||
// 测试无效会话
|
||||
t.Run("InvalidSession", func(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
clientReq := ClientRequest{
|
||||
Auth: auth{
|
||||
SecondaryPassword: validOtp,
|
||||
},
|
||||
}
|
||||
reqBody, _ := xml.Marshal(clientReq)
|
||||
|
||||
req := httptest.NewRequest("POST", "/otp-verification", bytes.NewReader(reqBody))
|
||||
req.AddCookie(&http.Cookie{Name: "auth-session-id", Value: "invalid-session"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
LinkAuth_otp(w, req)
|
||||
|
||||
ast.Equal(http.StatusUnauthorized, w.Code)
|
||||
})
|
||||
|
||||
// 测试缺少会话Cookie
|
||||
t.Run("MissingSessionCookie", func(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
|
||||
clientReq := ClientRequest{
|
||||
Auth: auth{
|
||||
SecondaryPassword: validOtp,
|
||||
},
|
||||
}
|
||||
reqBody, _ := xml.Marshal(clientReq)
|
||||
|
||||
req := httptest.NewRequest("POST", "/otp-verification", bytes.NewReader(reqBody))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
LinkAuth_otp(w, req)
|
||||
|
||||
ast.Equal(http.StatusUnauthorized, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateSession(t *testing.T) {
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("在GitHub Actions中跳过此测试")
|
||||
return
|
||||
}
|
||||
base.Test()
|
||||
ast := assert.New(t)
|
||||
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
// 创建测试数据
|
||||
group := "session-test-group"
|
||||
username := "session-test-user"
|
||||
|
||||
dns := []dbdata.ValData{{Val: "8.8.8.8"}}
|
||||
g := dbdata.Group{Name: group, Status: 1, ClientDns: dns}
|
||||
err := dbdata.SetGroup(&g)
|
||||
ast.Nil(err)
|
||||
|
||||
u := dbdata.User{Username: username, Groups: []string{group}, Status: 1}
|
||||
err = dbdata.SetUser(&u)
|
||||
ast.Nil(err)
|
||||
|
||||
// 创建认证会话数据
|
||||
authSession := &AuthSession{
|
||||
ClientRequest: &ClientRequest{
|
||||
Auth: auth{
|
||||
Username: username,
|
||||
},
|
||||
GroupSelect: group,
|
||||
UserAgent: "test-agent",
|
||||
DeviceId: deviceId{
|
||||
UniqueIdGlobal: "test-device-id",
|
||||
},
|
||||
MacAddressList: macAddressList{
|
||||
MacAddress: "00:11:22:33:44:55",
|
||||
},
|
||||
RemoteAddr: "192.168.1.100",
|
||||
},
|
||||
UserActLog: &dbdata.UserActLog{
|
||||
Username: username,
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
DeviceType: "test-device",
|
||||
PlatformVersion: "test-platform",
|
||||
},
|
||||
}
|
||||
|
||||
// 测试会话创建
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
req.RemoteAddr = "192.168.1.100:12345"
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
CreateSession(w, req, authSession)
|
||||
|
||||
ast.Equal(http.StatusOK, w.Code)
|
||||
// 验证响应包含会话信息
|
||||
ast.Contains(w.Body.String(), "session-token")
|
||||
}
|
||||
|
||||
func preIpData() {
|
||||
// 设置测试模式
|
||||
base.Test()
|
||||
|
||||
// 创建临时数据库文件
|
||||
tmpDb := path.Join(os.TempDir(), "anylink_otp_test.db")
|
||||
|
||||
// 设置数据库配置
|
||||
base.Cfg.DbType = "sqlite3"
|
||||
base.Cfg.DbSource = tmpDb
|
||||
|
||||
// 设置其他必要的配置
|
||||
base.Cfg.Ipv4CIDR = "192.168.3.0/24"
|
||||
base.Cfg.Ipv4Gateway = "192.168.3.1"
|
||||
base.Cfg.Ipv4Start = "192.168.3.100"
|
||||
base.Cfg.Ipv4End = "192.168.3.150"
|
||||
base.Cfg.MaxClient = 100
|
||||
base.Cfg.MaxUserClient = 3
|
||||
base.Cfg.IpLease = 5
|
||||
|
||||
// 启动数据库
|
||||
dbdata.Start()
|
||||
}
|
||||
|
||||
func closeIpdata() {
|
||||
_ = dbdata.Stop()
|
||||
tmpDb := path.Join(os.TempDir(), "anylink_otp_test.db")
|
||||
os.Remove(tmpDb)
|
||||
}
|
@@ -71,6 +71,12 @@ func startTls() {
|
||||
return dbdata.GetCertificateBySNI(chi.ServerName)
|
||||
},
|
||||
}
|
||||
// 开启证书认证
|
||||
if base.Cfg.AuthAloneCert {
|
||||
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven // 验证客户端证书
|
||||
tlsConfig.ClientCAs = dbdata.LoadClientCAPool() // 加载客户端CA证书
|
||||
base.Info("已启用独立证书验证")
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: initRoute(),
|
||||
|
@@ -2,13 +2,7 @@
|
||||
<el-card>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane label="邮件配置" name="dataSmtp">
|
||||
<el-form
|
||||
:model="dataSmtp"
|
||||
ref="dataSmtp"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
class="tab-one"
|
||||
>
|
||||
<el-form :model="dataSmtp" ref="dataSmtp" :rules="rules" label-width="100px" class="tab-one">
|
||||
<el-form-item label="服务器地址" prop="host">
|
||||
<el-input v-model="dataSmtp.host"></el-input>
|
||||
</el-form-item>
|
||||
@@ -19,11 +13,7 @@
|
||||
<el-input v-model="dataSmtp.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="dataSmtp.password"
|
||||
placeholder="密码为空则不修改"
|
||||
></el-input>
|
||||
<el-input type="password" v-model="dataSmtp.password" placeholder="密码为空则不修改"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="加密类型" prop="encryption">
|
||||
<el-radio-group v-model="dataSmtp.encryption">
|
||||
@@ -36,31 +26,18 @@
|
||||
<el-input v-model="dataSmtp.from"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm('dataSmtp')"
|
||||
>保存
|
||||
</el-button
|
||||
>
|
||||
<el-button type="primary" @click="submitForm('dataSmtp')">保存
|
||||
</el-button>
|
||||
<el-button @click="resetForm('dataSmtp')">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="审计日志" name="dataAuditLog">
|
||||
<el-form
|
||||
:model="dataAuditLog"
|
||||
ref="dataAuditLog"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
class="tab-one"
|
||||
>
|
||||
<el-form :model="dataAuditLog" ref="dataAuditLog" :rules="rules" label-width="100px" class="tab-one">
|
||||
<el-form-item label="审计去重间隔" prop="audit_interval">
|
||||
<el-input-number
|
||||
v-model="dataAuditLog.audit_interval"
|
||||
:min="-1"
|
||||
size="small"
|
||||
label="秒"
|
||||
:disabled="true"
|
||||
></el-input-number>
|
||||
<el-input-number v-model="dataAuditLog.audit_interval" :min="-1" size="small" label="秒"
|
||||
:disabled="true"></el-input-number>
|
||||
秒
|
||||
<p class="input_tip">
|
||||
请手动修改配置文件中的 audit_interval 参数后,再重启服务,
|
||||
@@ -68,13 +45,8 @@
|
||||
</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储时长" prop="life_day">
|
||||
<el-input-number
|
||||
v-model="dataAuditLog.life_day"
|
||||
:min="0"
|
||||
:max="365"
|
||||
size="small"
|
||||
label="天数"
|
||||
></el-input-number>
|
||||
<el-input-number v-model="dataAuditLog.life_day" :min="0" :max="365" size="small"
|
||||
label="天数"></el-input-number>
|
||||
天
|
||||
<p class="input_tip">
|
||||
范围: 0 ~ 365天 ,
|
||||
@@ -82,106 +54,50 @@
|
||||
</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="清理时间" prop="clear_time">
|
||||
<el-time-select
|
||||
v-model="dataAuditLog.clear_time"
|
||||
:picker-options="{
|
||||
start: '00:00',
|
||||
step: '01:00',
|
||||
end: '23:00',
|
||||
}"
|
||||
:editable="false"
|
||||
size="small"
|
||||
placeholder="请选择"
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-time-select v-model="dataAuditLog.clear_time" :picker-options="{
|
||||
start: '00:00',
|
||||
step: '01:00',
|
||||
end: '23:00',
|
||||
}" :editable="false" size="small" placeholder="请选择" style="width: 130px">
|
||||
</el-time-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm('dataAuditLog')"
|
||||
>保存
|
||||
</el-button
|
||||
>
|
||||
<el-button type="primary" @click="submitForm('dataAuditLog')">保存
|
||||
</el-button>
|
||||
<el-button @click="resetForm('dataAuditLog')">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="证书设置" name="datacertManage">
|
||||
<el-tabs
|
||||
tab-position="left"
|
||||
v-model="datacertManage"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tabs tab-position="left" v-model="datacertManage" @tab-click="handleClick">
|
||||
<el-tab-pane label="自定义证书" name="customCert">
|
||||
<el-form
|
||||
ref="customCert"
|
||||
:model="customCert"
|
||||
label-width="100px"
|
||||
size="small"
|
||||
class="tab-one"
|
||||
>
|
||||
<el-form ref="customCert" :model="customCert" label-width="100px" size="small" class="tab-one">
|
||||
<el-form-item>
|
||||
<el-upload
|
||||
class="uploadCert"
|
||||
:before-upload="beforeCertUpload"
|
||||
:action="certUpload"
|
||||
:limit="1"
|
||||
>
|
||||
<el-button size="mini" icon="el-icon-plus" slot="trigger"
|
||||
>证书文件
|
||||
</el-button
|
||||
>
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="请上传 .pem 格式的 cert 文件"
|
||||
placement="top"
|
||||
>
|
||||
<el-upload class="uploadCert" :before-upload="beforeCertUpload" :action="certUpload" :limit="1">
|
||||
<el-button size="mini" icon="el-icon-plus" slot="trigger">证书文件
|
||||
</el-button>
|
||||
<el-tooltip class="item" effect="dark" content="请上传 .pem 格式的 cert 文件" placement="top">
|
||||
<i class="el-icon-info"></i>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-upload
|
||||
class="uploadCert"
|
||||
:before-upload="beforeKeyUpload"
|
||||
:action="certUpload"
|
||||
:limit="1"
|
||||
>
|
||||
<el-button size="mini" icon="el-icon-plus" slot="trigger"
|
||||
>私钥文件
|
||||
</el-button
|
||||
>
|
||||
<el-tooltip
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="请上传 .pem 格式的 key 文件"
|
||||
placement="top"
|
||||
>
|
||||
<el-upload class="uploadCert" :before-upload="beforeKeyUpload" :action="certUpload" :limit="1">
|
||||
<el-button size="mini" icon="el-icon-plus" slot="trigger">私钥文件
|
||||
</el-button>
|
||||
<el-tooltip class="item" effect="dark" content="请上传 .pem 格式的 key 文件" placement="top">
|
||||
<i class="el-icon-info"></i>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-upload"
|
||||
type="primary"
|
||||
@click="submitForm('customCert')"
|
||||
>上传
|
||||
</el-button
|
||||
>
|
||||
<el-button size="small" icon="el-icon-upload" type="primary" @click="submitForm('customCert')">上传
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Let's Encrypt证书" name="letsCert">
|
||||
<el-form
|
||||
:model="letsCert"
|
||||
ref="letsCert"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
size="small"
|
||||
class="tab-one"
|
||||
>
|
||||
<el-form :model="letsCert" ref="letsCert" :rules="rules" label-width="120px" size="small" class="tab-one">
|
||||
<el-form-item label="域名" prop="domain">
|
||||
<el-input v-model="letsCert.domain"></el-input>
|
||||
</el-form-item>
|
||||
@@ -195,77 +111,109 @@
|
||||
<el-radio label="cfcloud">cloudflare</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-for="component in dnsProvider[letsCert.name]"
|
||||
:key="component.prop"
|
||||
:label="component.label"
|
||||
:rules="component.rules"
|
||||
>
|
||||
<component
|
||||
:is="component.component"
|
||||
:type="component.type"
|
||||
v-model="letsCert[letsCert.name][component.prop]"
|
||||
></component>
|
||||
<el-form-item v-for="component in dnsProvider[letsCert.name]" :key="component.prop"
|
||||
:label="component.label" :rules="component.rules">
|
||||
<component :is="component.component" :type="component.type"
|
||||
v-model="letsCert[letsCert.name][component.prop]"></component>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-switch
|
||||
style="display: block"
|
||||
v-model="letsCert.renew"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
inactive-text="自动续期"
|
||||
>
|
||||
<el-switch style="display: block" v-model="letsCert.renew" active-color="#13ce66"
|
||||
inactive-color="#ff4949" inactive-text="自动续期">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm('letsCert')"
|
||||
>申请
|
||||
</el-button
|
||||
>
|
||||
<el-button type="primary" @click="submitForm('letsCert')">申请
|
||||
</el-button>
|
||||
<el-button @click="resetForm('letsCert')">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="客户端证书" name="clientCert">
|
||||
<el-form ref="clientCert" :model="clientCert" label-width="80px" size="small" class="tab-one">
|
||||
<!-- 生成证书对话框 -->
|
||||
<el-dialog title="生成客户端证书" :visible.sync="generateCertDialog" width="450px">
|
||||
<el-form :model="generateForm" label-width="80px">
|
||||
<el-form-item label="用户名">
|
||||
<el-select v-model="generateForm.username" placeholder="请输入或选择用户名" filterable clearable allow-create
|
||||
default-first-option style="width: 100%;" @change="onUserChange"
|
||||
popper-class="custom-select-dropdown">
|
||||
<el-option v-for="user in userList" :key="user.username" :label="user.username"
|
||||
:value="user.username">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 添加用户组选择 -->
|
||||
<el-form-item label="用户组" v-if="userGroups.length > 0">
|
||||
<el-select v-model="generateForm.groupName" placeholder="请选择用户组" style="width: 100%;">
|
||||
<el-option v-for="group in userGroups" :key="group" :label="group" :value="group">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="generateCertDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGenerateCert">确定生成</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<el-form-item>
|
||||
<el-button type="warning" @click="initClientCA">初始化客户端 CA</el-button>
|
||||
<el-tooltip content="首次使用前需要初始化客户端CA,初始化客户端 CA 将会使所有现有的客户端证书失效,初始化后请重启服务!!!" placement="top">
|
||||
<i class="el-icon-info"></i>
|
||||
</el-tooltip>
|
||||
<el-button type="primary" @click="generateClientCert" style="margin-left: 10px;">生成证书</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="clientCertList" style="width: 100%" border>
|
||||
<el-table-column prop="username" label="用户名"></el-table-column>
|
||||
<el-table-column prop="groupname" label="用户组"></el-table-column>
|
||||
<el-table-column prop="serial_number" label="序列号"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" :formatter="dateFormat"></el-table-column>
|
||||
<el-table-column prop="not_after" label="过期时间" :formatter="dateFormat"></el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="downloadCert(scope.row)">下载</el-button>
|
||||
<el-button size="mini" :type="scope.row.status === 0 ? 'warning' : 'success'"
|
||||
@click="changeCertStatus(scope.row)" :disabled="scope.row.status === 2">
|
||||
{{ scope.row.status === 0 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteCert(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
||||
:current-page="pagination.current" :page-sizes="[10, 20, 50, 100]" :page-size="pagination.size"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="pagination.total">
|
||||
</el-pagination>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="其他设置" name="dataOther">
|
||||
<el-form
|
||||
:model="dataOther"
|
||||
ref="dataOther"
|
||||
:rules="rules"
|
||||
label-width="130px"
|
||||
class="tab-one"
|
||||
>
|
||||
<el-form :model="dataOther" ref="dataOther" :rules="rules" label-width="130px" class="tab-one">
|
||||
<el-form-item label="vpn对外地址" prop="link_addr">
|
||||
<el-input placeholder="请输入内容" v-model="dataOther.link_addr">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Banner信息" prop="banner">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入内容"
|
||||
v-model="dataOther.banner"
|
||||
>
|
||||
<el-input type="textarea" :rows="5" placeholder="请输入内容" v-model="dataOther.banner">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="自定义首页状态码" prop="homecode">
|
||||
<el-input-number
|
||||
v-model="dataOther.homecode"
|
||||
:min="0"
|
||||
:max="1000"
|
||||
></el-input-number>
|
||||
<el-input-number v-model="dataOther.homecode" :min="0" :max="1000"></el-input-number>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="自定义首页" prop="homeindex">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入内容"
|
||||
v-model="dataOther.homeindex"
|
||||
>
|
||||
<el-input type="textarea" :rows="10" placeholder="请输入内容" v-model="dataOther.homeindex">
|
||||
</el-input>
|
||||
<el-tooltip content="自定义内容可以参考 index_template 目录下的文件" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
@@ -273,29 +221,18 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="账户开通邮件模板" prop="account_mail">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入内容"
|
||||
v-model="dataOther.account_mail"
|
||||
>
|
||||
<el-input type="textarea" :rows="10" placeholder="请输入内容" v-model="dataOther.account_mail">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮件展示">
|
||||
<iframe
|
||||
width="500px"
|
||||
height="300px"
|
||||
:srcdoc="dataOther.account_mail"
|
||||
>
|
||||
<iframe width="500px" height="300px" :srcdoc="dataOther.account_mail">
|
||||
</iframe>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm('dataOther')"
|
||||
>保存
|
||||
</el-button
|
||||
>
|
||||
<el-button type="primary" @click="submitForm('dataOther')">保存
|
||||
</el-button>
|
||||
<el-button @click="resetForm('dataOther')">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -339,19 +276,19 @@ export default {
|
||||
authToken: "",
|
||||
},
|
||||
},
|
||||
customCert: {cert: "", key: ""},
|
||||
customCert: { cert: "", key: "" },
|
||||
dataOther: {},
|
||||
rules: {
|
||||
host: {required: true, message: "请输入服务器地址", trigger: "blur"},
|
||||
host: { required: true, message: "请输入服务器地址", trigger: "blur" },
|
||||
port: [
|
||||
{required: true, message: "请输入服务器端口", trigger: "blur"},
|
||||
{ required: true, message: "请输入服务器端口", trigger: "blur" },
|
||||
{
|
||||
type: "number",
|
||||
message: "请输入正确的服务器端口",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
],
|
||||
issuer: {required: true, message: "请输入系统名称", trigger: "blur"},
|
||||
issuer: { required: true, message: "请输入系统名称", trigger: "blur" },
|
||||
domain: {
|
||||
required: true,
|
||||
message: "请输入需要申请证书的域名",
|
||||
@@ -362,7 +299,7 @@ export default {
|
||||
message: "请输入申请证书的邮箱地址",
|
||||
trigger: "blur",
|
||||
},
|
||||
name: {required: true, message: "请选择域名服务商", trigger: "blur"},
|
||||
name: { required: true, message: "请选择域名服务商", trigger: "blur" },
|
||||
},
|
||||
certUpload: "/set/other/customcert",
|
||||
dnsProvider: {
|
||||
@@ -428,6 +365,20 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
generateCertDialog: false,
|
||||
generateForm: {
|
||||
username: '',
|
||||
groupName: ''
|
||||
},
|
||||
userList: [],
|
||||
userGroups: [],
|
||||
allGroups: [],
|
||||
clientCertList: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -443,6 +394,9 @@ export default {
|
||||
case "letsCert":
|
||||
this.getletsCert();
|
||||
break;
|
||||
case "clientCert":
|
||||
this.loadClientCertList();
|
||||
break;
|
||||
case "dataOther":
|
||||
this.getOther();
|
||||
break;
|
||||
@@ -464,71 +418,293 @@ export default {
|
||||
},
|
||||
getSmtp() {
|
||||
axios
|
||||
.get("/set/other/smtp")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataSmtp = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
.get("/set/other/smtp")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataSmtp = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
getAuditLog() {
|
||||
axios
|
||||
.get("/set/other/audit_log")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataAuditLog = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
.get("/set/other/audit_log")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataAuditLog = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
getletsCert() {
|
||||
axios
|
||||
.get("/set/other/getcertset")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.letsCert = Object.assign({}, this.letsCert, rdata.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
.get("/set/other/getcertset")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.letsCert = Object.assign({}, this.letsCert, rdata.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
// 初始化客户端 CA
|
||||
initClientCA() {
|
||||
this.$confirm('确定要初始化客户端 CA 吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
axios.post('/set/client_cert/init_ca').then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.$message.success('客户端 CA 初始化成功');
|
||||
} else {
|
||||
this.$message.error(resp.data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onUserChange(username) {
|
||||
this.generateForm.groupName = '';
|
||||
this.userGroups = [];
|
||||
|
||||
if (username) {
|
||||
const selectedUser = this.userList.find(user => user.username === username);
|
||||
if (selectedUser && selectedUser.groups) {
|
||||
this.userGroups = selectedUser.groups;
|
||||
if (this.userGroups.length === 1) {
|
||||
this.generateForm.groupName = this.userGroups[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 生成客户端证书
|
||||
generateClientCert() {
|
||||
this.generateCertDialog = true;
|
||||
this.generateForm = { username: '', groupName: '' };
|
||||
this.userGroups = [];
|
||||
|
||||
axios.get('/set/client_cert/user_cert_info').then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.userList = resp.data.data.users || [];
|
||||
this.allGroups = resp.data.data.groups || [];
|
||||
} else {
|
||||
this.$message.error(resp.data.msg);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('加载用户和组信息失败:', error);
|
||||
this.$message.error('加载用户和组信息失败');
|
||||
});
|
||||
},
|
||||
confirmGenerateCert() {
|
||||
if (!this.generateForm.username) {
|
||||
this.$message.error('请选择或输入用户名');
|
||||
return;
|
||||
}
|
||||
if (this.userGroups.length > 0 && !this.generateForm.groupName) {
|
||||
this.$message.error('请选择用户组');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('username', this.generateForm.username);
|
||||
if (this.generateForm.groupName) {
|
||||
formData.append('group_name', this.generateForm.groupName);
|
||||
}
|
||||
|
||||
axios.post('/set/client_cert/generate', formData).then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.$message.success('证书生成成功');
|
||||
this.generateCertDialog = false;
|
||||
this.loadClientCertList();
|
||||
} else {
|
||||
this.$message.error(resp.data.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
downloadCert(row) {
|
||||
this.$prompt('请输入证书密码,留空则不使用密码:', {
|
||||
confirmButtonText: '下载',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: '',
|
||||
inputType: 'password',
|
||||
inputPlaceholder: '留空则不使用密码',
|
||||
}).then(({ value }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('username', row.username);
|
||||
params.append('password', value || '');
|
||||
|
||||
axios({
|
||||
method: 'get',
|
||||
url: '/set/client_cert/download?' + params.toString(),
|
||||
responseType: 'blob'
|
||||
}).then(response => {
|
||||
const contentType = response.headers['content-type'];
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const errorData = JSON.parse(reader.result);
|
||||
this.$message.error(errorData.msg || '证书下载失败');
|
||||
} catch (e) {
|
||||
this.$message.error('证书下载失败');
|
||||
}
|
||||
};
|
||||
reader.readAsText(response.data);
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([response.data], { type: 'application/x-pkcs12' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${row.username}.p12`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
this.$message.success('证书下载成功');
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.data) {
|
||||
this.$message.error(error.response.data.msg || '证书下载失败');
|
||||
} else {
|
||||
this.$message.error('证书下载失败');
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消下载');
|
||||
});
|
||||
},
|
||||
// 加载证书列表
|
||||
loadClientCertList() {
|
||||
const params = {
|
||||
page_size: this.pagination.size,
|
||||
page_index: this.pagination.current
|
||||
};
|
||||
|
||||
axios.get('/set/client_cert/list', { params }).then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.clientCertList = resp.data.data.list;
|
||||
this.pagination.total = resp.data.data.total;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 分页处理
|
||||
handleSizeChange(val) {
|
||||
this.pagination.size = val;
|
||||
this.loadClientCertList();
|
||||
},
|
||||
|
||||
handleCurrentChange(val) {
|
||||
this.pagination.current = val;
|
||||
this.loadClientCertList();
|
||||
},
|
||||
|
||||
handleUserSelect(item) {
|
||||
this.generateForm.username = item.username;
|
||||
},
|
||||
// 日期格式化
|
||||
dateFormat(row, column, cellValue) {
|
||||
return new Date(cellValue).toLocaleString();
|
||||
},
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
0: '启用',
|
||||
1: '禁用',
|
||||
2: '过期'
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
},
|
||||
|
||||
// 获取状态类型(用于标签颜色)
|
||||
getStatusType(status) {
|
||||
const typeMap = {
|
||||
0: 'success', // 启用 - 绿色
|
||||
1: 'warning', // 禁用 - 橙色
|
||||
2: 'danger' // 过期 - 红色
|
||||
};
|
||||
return typeMap[status] || '';
|
||||
},
|
||||
// 切换证书状态
|
||||
changeCertStatus(row) {
|
||||
const action = row.status === 0 ? '禁用' : '启用';
|
||||
this.$confirm(`确定要${action}用户 ${row.username} 的证书吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', row.username);
|
||||
|
||||
axios.post('/set/client_cert/changecertstatus', formData).then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.$message.success(`证书${action}成功`);
|
||||
this.loadClientCertList();
|
||||
} else {
|
||||
this.$message.error(resp.data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
// 删除证书
|
||||
deleteCert(row) {
|
||||
this.$confirm(`确定要删除用户 ${row.username} 的证书吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', row.username);
|
||||
|
||||
axios.post('/set/client_cert/delete', formData).then(resp => {
|
||||
if (resp.data.code === 0) {
|
||||
this.$message.success('证书删除成功');
|
||||
this.loadClientCertList();
|
||||
} else {
|
||||
this.$message.error(resp.data.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getOther() {
|
||||
axios
|
||||
.get("/set/other")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataOther = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
.get("/set/other")
|
||||
.then((resp) => {
|
||||
let rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code !== 0) {
|
||||
this.$message.error(rdata.msg);
|
||||
return;
|
||||
}
|
||||
this.dataOther = rdata.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$message.error("哦,请求出错");
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
submitForm(formName) {
|
||||
this.$refs[formName].validate((valid) => {
|
||||
@@ -550,16 +726,16 @@ export default {
|
||||
break;
|
||||
case "dataAuditLog":
|
||||
axios
|
||||
.post("/set/other/audit_log/edit", this.dataAuditLog)
|
||||
.then((resp) => {
|
||||
var rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code === 0) {
|
||||
this.$message.success(rdata.msg);
|
||||
} else {
|
||||
this.$message.error(rdata.msg);
|
||||
}
|
||||
});
|
||||
.post("/set/other/audit_log/edit", this.dataAuditLog)
|
||||
.then((resp) => {
|
||||
var rdata = resp.data;
|
||||
console.log(rdata);
|
||||
if (rdata.code === 0) {
|
||||
this.$message.success(rdata.msg);
|
||||
} else {
|
||||
this.$message.error(rdata.msg);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "letsCert":
|
||||
var loading = this.$loading({
|
||||
@@ -624,4 +800,14 @@ export default {
|
||||
line-height: 1.428;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
/* 自定义下拉框样式 */
|
||||
.custom-select-dropdown .el-select-dropdown__wrap {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-select-dropdown .el-select-dropdown__list {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user