新增客户端证书认证的功能

This commit is contained in:
wsczx
2025-08-19 20:27:41 +08:00
parent 315e1deadc
commit 118fcb3b60
13 changed files with 1089 additions and 284 deletions

View File

@@ -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,209 @@ 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
}
// 检查用户是否存在
user := &dbdata.User{}
err := dbdata.One("Username", username, user)
if err != nil {
RespError(w, RespInternalErr, "用户不存在")
return
}
// 生成客户端证书
certData, err := dbdata.GenerateClientCert(username)
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)
}

View File

@@ -59,6 +59,14 @@ 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("/user/list", UserList)
r.HandleFunc("/user/detail", UserDetail)

View File

@@ -34,29 +34,31 @@ 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"`
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

View File

@@ -33,6 +33,8 @@ 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: 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"},

View File

@@ -20,6 +20,7 @@
<ExtendedKeyUsage>
<ExtendedMatchKey>ClientAuth</ExtendedMatchKey>
</ExtendedKeyUsage>
<CertificateStore>User</CertificateStore>
</CertificateMatch>
</ClientInitialization>

View File

@@ -11,6 +11,11 @@ cert_file = "./conf/vpn_cert.pem"
cert_key = "./conf/vpn_cert.key"
files_path = "./conf/files"
#客户端证书CA证书
client_cert_ca_file = "./conf/client_ca.pem"
#客户端证书CA密钥
client_cert_ca_key_file = "./conf/client_ca.key"
#日志目录,默认为空写入标准输出
#log_path = "./log"
log_level = "debug"

View File

@@ -0,0 +1,429 @@
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:"group_name" xorm:"varchar(60)"`
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"`
Status int `json:"status" xorm:"int default 0"`
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 {
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 string) (*ClientCertData, error) {
// 检查是否已存在证书记录
_, 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,
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,
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.GetStatus() != CertStatusActive {
base.Error("证书验证失败:证书状态为", 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
}

View File

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

View File

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

View File

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

View File

@@ -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,25 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
return
}
// 检查客户端证书认证
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
clientCert := r.TLS.PeerCertificates[0]
username := clientCert.Subject.CommonName
// 验证证书有效性和用户状态
if dbdata.ValidateClientCert(clientCert, userAgent) {
// 证书认证成功,创建会话
base.Info("用户通过证书认证:", username)
ua.Username = username
ua.Info = "用户通过证书认证登录"
ua.Status = dbdata.UserConnected
dbdata.UserActLogIns.Add(*ua, userAgent)
CreateSession(w, r, sessionData)
return
}
}
if cr.Type == "init" {
w.WriteHeader(http.StatusOK)
@@ -84,22 +116,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) // 记录登录失败状态

View File

@@ -66,6 +66,8 @@ func startTls() {
NextProtos: []string{"http/1.1"},
MinVersion: tls.VersionTLS12,
CipherSuites: selectedCipherSuites,
ClientAuth: tls.VerifyClientCertIfGiven, // 验证客户端证书
ClientCAs: dbdata.LoadClientCAPool(), // 加载客户端CA证书
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
base.Trace("GetCertificate ServerName", chi.ServerName)
return dbdata.GetCertificateBySNI(chi.ServerName)