feat(dcauth): 移除机器码,改用纯设备码认证
- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426 - 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示 - 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件 - Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态 - 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
This commit is contained in:
+34
-7
@@ -5,7 +5,7 @@
|
||||
## 运行方式
|
||||
|
||||
- **Windows**:GUI 常驻程序。启动后进入系统托盘,通过协议唤起时只出现一个登录确认窗口;成功后在托盘气泡中提示,不再连续弹多个消息框。
|
||||
- **macOS**:后台常驻进程,使用系统图形对话框确认登录。重复执行命令会转发给已有进程,不会叠加实例。
|
||||
- **macOS**:`DCAuth.app` 注册 `dcauth://`,浏览器可直接唤起;内部是后台常驻进程 + 系统图形确认框,重复唤起会转发给已有进程,不会叠加实例。
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -18,9 +18,10 @@ x86_64-w64-mingw32-windres --codepage=65001 versioninfo.rc \
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
||||
go build -trimpath -ldflags "-H windowsgui -s -w" \
|
||||
-o dist/DCAuth.exe .
|
||||
cp dist/DCAuth.exe dist/DCAuth-2.0.0.exe
|
||||
```
|
||||
|
||||
版本号统一在 `main.go` 的 `clientVersion` 和 `versioninfo.rc` 中维护。`DCAuth.exe` 右键“属性 → 详细信息”会显示文件版本和产品版本,托盘菜单、设备信息及确认窗口也会显示当前版本。
|
||||
版本号统一在 `main.go` 的 `clientVersion` 和 `versioninfo.rc` 中维护。`DCAuth.exe` 右键“属性 → 详细信息”会显示文件版本 `2.0.0.0` 和产品版本 `2.0.0`,托盘菜单、设备信息及确认窗口也会显示当前版本。
|
||||
|
||||
Windows ARM64:
|
||||
|
||||
@@ -56,12 +57,38 @@ codesign --force --sign - dist/DCAuth-macos
|
||||
|
||||
托盘操作:
|
||||
|
||||
- 双击图标:查看设备码和机器码,并复制到剪贴板。
|
||||
- 双击图标:查看设备码并复制到剪贴板。
|
||||
- 右键菜单:查看设备码、退出 DCAuth。
|
||||
- 浏览器登录:只弹出一个确认窗口;点击“确定”后显示成功气泡,点击“取消”则拒绝。
|
||||
- 再次唤起协议:第二个进程会把请求转交给常驻进程,然后立即退出。
|
||||
|
||||
## macOS 开发调试
|
||||
## macOS 安装(一次性)
|
||||
|
||||
```bash
|
||||
cd apps/dcauth
|
||||
./build-macos.sh # 默认装到 ~/Applications/DCAuth.app
|
||||
```
|
||||
|
||||
脚本会做四件事:
|
||||
|
||||
1. 用 `go build` 产出当前架构的二进制(自动读取 `main.go` 的 `clientVersion`)。
|
||||
2. 用 `osacompile` 生成 AppleScript 壳,把 `dcauth://` 事件转成 argv 交给二进制。
|
||||
3. 向 LaunchServices 注册 `dcauth://` 协议,并做 ad-hoc 签名。
|
||||
4. 打印安装路径与退出方式。
|
||||
|
||||
装好之后浏览器登录页点一次即可直接唤起确认框,和 Windows 一样,**不再需要命令行**。
|
||||
|
||||
- 查看设备码:`open "$HOME/Applications/DCAuth.app"`
|
||||
- 停止后台进程:`"$HOME/Applications/DCAuth.app/Contents/Resources/dcauth" --exit`
|
||||
- 换成 Apple 芯片 / Intel 通用包:脚本在目标机器上各跑一次即可(二进制与架构绑定)。
|
||||
|
||||
> 为什么要套一层 AppleScript:macOS 只把自定义协议 URL 作为 Apple Event 投递,
|
||||
> 纯 Go 的命令行程序收不到(实测进程参数为空)。AppleScript applet 的
|
||||
> `on open location` 是目前不引入 Cocoa/cgo 的最稳做法。
|
||||
|
||||
## macOS 命令行方式(备用)
|
||||
|
||||
没有安装 `.app` 时,也可以直接执行二进制:
|
||||
|
||||
登录页在开发模式会显示一条 `dcauth://...` 链接。复制后在终端执行:
|
||||
|
||||
@@ -78,7 +105,7 @@ cd apps/dcauth
|
||||
./dist/DCAuth-macos --exit
|
||||
```
|
||||
|
||||
macOS 当前仍是开发版:私钥明文保存在用户配置目录的 `DCAuth/device.key`,且浏览器不会自动拉起程序,需要从登录页复制命令执行。
|
||||
macOS 当前仍是开发版:私钥明文保存在 `~/Library/Application Support/DCAuth/device.key`(没有 DPAPI 等系统级保护),不要把这一版当成生产级凭证。安装 `DCAuth.app` 后浏览器可以自动唤起,但密钥保护强度仍低于 Windows。
|
||||
|
||||
如果只是测后台业务,不想每次都走短信和设备验证,可以在“后台用户”里把测试账号设为 **技术测试**。该账号只使用账号密码登录;正式账号仍必须走短信 + DCAuth。
|
||||
|
||||
@@ -118,7 +145,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-H windowsg
|
||||
## 安全说明
|
||||
|
||||
- 私钥不离开本机,Windows 使用 DPAPI 保护。
|
||||
- 服务端只保存公钥和机器码哈希/脱敏值。
|
||||
- 机器码采集主板 UUID、MachineGuid、主板/BIOS/系统盘序列号、CPU ID,不上传原始序列号。
|
||||
- 服务端只保存设备公钥和设备码,不采集或保存硬件序列号。
|
||||
- 设备码只由公钥派生,不包含硬件指纹;更换硬件不会改变设备码。
|
||||
- 正式分发前建议使用代码签名证书,避免 SmartScreen 和杀毒软件误报。
|
||||
- 如果要做到 token 也不能复制,需要后续让 exe 对每次 API 请求签名,或把 exe 作为唯一客户端。
|
||||
|
||||
@@ -2,8 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -15,25 +13,6 @@ func buildDeviceID(publicKey []byte) string {
|
||||
return formatCode("D1", sum[:])
|
||||
}
|
||||
|
||||
func buildMachineCode() (string, error) {
|
||||
components, err := collectHardwareComponents()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keys := make([]string, 0, len(components))
|
||||
for key := range components {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
parts := []string{"dcauth.machine.v1"}
|
||||
for _, key := range keys {
|
||||
parts = append(parts, key+"="+components[key])
|
||||
}
|
||||
sum := sha256.Sum256([]byte(strings.Join(parts, "\n")))
|
||||
return formatCode("M1", sum[:]), nil
|
||||
}
|
||||
|
||||
func formatCode(prefix string, data []byte) string {
|
||||
encoded := base32Crockford(data)
|
||||
if len(encoded) > 20 {
|
||||
@@ -68,22 +47,3 @@ func base32Crockford(data []byte) string {
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func normalizeComponent(value string) string {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(value))
|
||||
normalized = strings.Join(strings.Fields(normalized), "")
|
||||
switch normalized {
|
||||
case "", "0", "NONE", "N/A", "NA", "UNKNOWN", "DEFAULTSTRING",
|
||||
"TOBEFILLEDBYO.E.M.", "TOBEFILLEDBYOEM", "SYSTEMSERIALNUMBER",
|
||||
"00000000-0000-0000-0000-000000000000":
|
||||
return ""
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func ensureNotEmpty(components map[string]string) error {
|
||||
if len(components) == 0 {
|
||||
return fmt.Errorf("无法读取本机硬件信息,请联系管理员")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 macOS 版 DCAuth.app,并向 LaunchServices 注册 dcauth:// 协议。
|
||||
#
|
||||
# 为什么需要 .app:macOS 只把自定义协议的 URL 以 Apple Event 形式投递,
|
||||
# 非 Cocoa 的纯命令行程序拿不到(实测 argv 里没有 URL)。这里用 AppleScript
|
||||
# applet 的 on open location 接住事件,再把 URL 作为 argv 交给 Go 二进制,
|
||||
# 从而让浏览器点一下就能唤起本机确认窗口,不需要手动执行命令。
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "[build-macos] 只支持在 macOS 上运行" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_PATH="${1:-$HOME/Applications/DCAuth.app}"
|
||||
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||
|
||||
VERSION="$(sed -n 's/^const clientVersion = "\(.*\)"$/\1/p' "$ROOT/main.go")"
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "[build-macos] 无法从 main.go 读取 clientVersion" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
echo "[build-macos] 编译 DCAuth v$VERSION"
|
||||
(
|
||||
cd "$ROOT"
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$WORK_DIR/dcauth" .
|
||||
)
|
||||
|
||||
echo "[build-macos] 生成 AppleScript 壳"
|
||||
cat > "$WORK_DIR/main.applescript" <<'APPLESCRIPT'
|
||||
on run
|
||||
set bundlePath to POSIX path of (path to me)
|
||||
do shell script quoted form of (bundlePath & "Contents/Resources/dcauth")
|
||||
end run
|
||||
|
||||
on open location theURL
|
||||
set bundlePath to POSIX path of (path to me)
|
||||
do shell script quoted form of (bundlePath & "Contents/Resources/dcauth") & " " & quoted form of theURL
|
||||
end open location
|
||||
APPLESCRIPT
|
||||
|
||||
rm -rf "$APP_PATH"
|
||||
mkdir -p "$(dirname "$APP_PATH")"
|
||||
osacompile -o "$APP_PATH" "$WORK_DIR/main.applescript"
|
||||
mkdir -p "$APP_PATH/Contents/Resources"
|
||||
install -m 0755 "$WORK_DIR/dcauth" "$APP_PATH/Contents/Resources/dcauth"
|
||||
|
||||
PLIST="$APP_PATH/Contents/Info.plist"
|
||||
plist_set_string() {
|
||||
local entry="$1"
|
||||
local value="$2"
|
||||
# osacompile 生成的 plist 只有少数键,Set 失败时要带类型 Add。
|
||||
/usr/libexec/PlistBuddy -c "Set $entry $value" "$PLIST" 2>/dev/null ||
|
||||
/usr/libexec/PlistBuddy -c "Add $entry string $value" "$PLIST"
|
||||
}
|
||||
plist_delete() {
|
||||
/usr/libexec/PlistBuddy -c "Delete $1" "$PLIST" 2>/dev/null || true
|
||||
}
|
||||
|
||||
plist_set_string ':CFBundleIdentifier' 'com.khhao.dcauth'
|
||||
plist_set_string ':CFBundleName' 'DCAuth'
|
||||
plist_set_string ':CFBundleDisplayName' 'DCAuth'
|
||||
plist_set_string ':CFBundleShortVersionString' "$VERSION"
|
||||
plist_set_string ':CFBundleVersion' "$VERSION"
|
||||
|
||||
plist_delete ':CFBundleURLTypes'
|
||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes array' "$PLIST"
|
||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0 dict' "$PLIST"
|
||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLName string com.khhao.dcauth' "$PLIST"
|
||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLSchemes array' "$PLIST"
|
||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string dcauth' "$PLIST"
|
||||
|
||||
# 后台常驻,不显示 Dock 图标;双击 .app 等同于"启动/查看设备码"。
|
||||
plist_delete ':LSUIElement'
|
||||
/usr/libexec/PlistBuddy -c 'Add :LSUIElement bool true' "$PLIST"
|
||||
|
||||
echo "[build-macos] 签名并注册协议"
|
||||
codesign --force --deep --sign - "$APP_PATH" >/dev/null 2>&1
|
||||
if [[ -x "$LSREGISTER" ]]; then
|
||||
"$LSREGISTER" -f "$APP_PATH"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "[build-macos] 完成:$APP_PATH"
|
||||
echo "[build-macos] 浏览器再次点击登录即可直接唤起 DCAuth(无需命令行)。"
|
||||
echo "[build-macos] 查看设备码:open \"$APP_PATH\""
|
||||
echo "[build-macos] 停止后台进程:\"$APP_PATH/Contents/Resources/dcauth\" --exit"
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func collectHardwareComponents() (map[string]string, error) {
|
||||
hostname, _ := os.Hostname()
|
||||
components := map[string]string{
|
||||
"platform": runtime.GOOS + "-" + runtime.GOARCH,
|
||||
}
|
||||
if normalized := normalizeComponent(hostname); normalized != "" {
|
||||
components["hostname"] = normalized
|
||||
}
|
||||
return components, ensureNotEmpty(components)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func collectHardwareComponents() (map[string]string, error) {
|
||||
components := map[string]string{}
|
||||
|
||||
script := `
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
$cs = Get-CimInstance Win32_ComputerSystemProduct
|
||||
$bb = Get-CimInstance Win32_BaseBoard
|
||||
$bios = Get-CimInstance Win32_BIOS
|
||||
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
||||
$disk = Get-CimInstance Win32_DiskDrive | Where-Object { $_.Index -eq 0 } | Select-Object -First 1
|
||||
[pscustomobject]@{
|
||||
smbiosUuid = [string]$cs.UUID
|
||||
baseboardSerial = [string]$bb.SerialNumber
|
||||
biosSerial = [string]$bios.SerialNumber
|
||||
cpuId = [string]$cpu.ProcessorId
|
||||
systemDiskSerial = [string]$disk.SerialNumber
|
||||
} | ConvertTo-Json -Compress
|
||||
`
|
||||
command := exec.Command(
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command", script,
|
||||
)
|
||||
hideCommandWindow(command)
|
||||
output, err := command.Output()
|
||||
if err == nil {
|
||||
var payload map[string]string
|
||||
if json.Unmarshal([]byte(strings.TrimSpace(string(output))), &payload) == nil {
|
||||
for key, value := range payload {
|
||||
if normalized := normalizeComponent(value); normalized != "" {
|
||||
components[key] = normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if machineGUID := readMachineGUID(); machineGUID != "" {
|
||||
components["machineGuid"] = machineGUID
|
||||
}
|
||||
if err := ensureNotEmpty(components); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return components, nil
|
||||
}
|
||||
|
||||
func readMachineGUID() string {
|
||||
command := exec.Command(
|
||||
"reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid",
|
||||
)
|
||||
hideCommandWindow(command)
|
||||
output, err := command.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
line := strings.TrimSpace(string(output))
|
||||
for _, candidate := range strings.Split(line, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(candidate))
|
||||
if len(fields) >= 3 && strings.EqualFold(fields[0], "MachineGuid") {
|
||||
return normalizeComponent(strings.Join(fields[2:], " "))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+19
-24
@@ -15,7 +15,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const clientVersion = "1.1.0"
|
||||
const clientVersion = "2.0.0"
|
||||
|
||||
type appConfig struct {
|
||||
DisplayName string
|
||||
@@ -58,10 +58,9 @@ var appRegistry = map[string]appConfig{
|
||||
}
|
||||
|
||||
type identity struct {
|
||||
privateKey ed25519.PrivateKey
|
||||
publicKey ed25519.PublicKey
|
||||
deviceID string
|
||||
machineCode string
|
||||
privateKey ed25519.PrivateKey
|
||||
publicKey ed25519.PublicKey
|
||||
deviceID string
|
||||
}
|
||||
|
||||
type authorizeRequest struct {
|
||||
@@ -69,7 +68,6 @@ type authorizeRequest struct {
|
||||
ChallengeID string `json:"challengeId"`
|
||||
Challenge string `json:"challenge"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
MachineCode string `json:"machineCode"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
Signature string `json:"signature"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
@@ -170,15 +168,7 @@ func containsOrigin(allowed []string, origin string) bool {
|
||||
|
||||
func submitAuthorization(id identity, target authorizeTarget) error {
|
||||
timestamp := time.Now().Unix()
|
||||
canonical := strings.Join([]string{
|
||||
"dcauth.v1",
|
||||
target.App,
|
||||
target.ChallengeID,
|
||||
target.Challenge,
|
||||
id.deviceID,
|
||||
id.machineCode,
|
||||
fmt.Sprintf("%d", timestamp),
|
||||
}, "\n")
|
||||
canonical := buildAuthorizationCanonical(id, target, timestamp)
|
||||
signature := ed25519.Sign(id.privateKey, []byte(canonical))
|
||||
|
||||
payload := authorizeRequest{
|
||||
@@ -186,7 +176,6 @@ func submitAuthorization(id identity, target authorizeTarget) error {
|
||||
ChallengeID: target.ChallengeID,
|
||||
Challenge: target.Challenge,
|
||||
DeviceID: id.deviceID,
|
||||
MachineCode: id.machineCode,
|
||||
PublicKey: base64.RawURLEncoding.EncodeToString(id.publicKey),
|
||||
Signature: base64.RawURLEncoding.EncodeToString(signature),
|
||||
Timestamp: timestamp,
|
||||
@@ -217,6 +206,17 @@ func submitAuthorization(id identity, target authorizeTarget) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildAuthorizationCanonical(id identity, target authorizeTarget, timestamp int64) string {
|
||||
return strings.Join([]string{
|
||||
"dcauth.v2",
|
||||
target.App,
|
||||
target.ChallengeID,
|
||||
target.Challenge,
|
||||
id.deviceID,
|
||||
fmt.Sprintf("%d", timestamp),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func summarizeResponse(body []byte) string {
|
||||
var payload map[string]any
|
||||
if json.Unmarshal(body, &payload) == nil {
|
||||
@@ -287,14 +287,9 @@ func loadOrCreateIdentity() (identity, error) {
|
||||
}
|
||||
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
machineCode, err := buildMachineCode()
|
||||
if err != nil {
|
||||
return identity{}, err
|
||||
}
|
||||
return identity{
|
||||
privateKey: privateKey,
|
||||
publicKey: publicKey,
|
||||
deviceID: buildDeviceID(publicKey),
|
||||
machineCode: machineCode,
|
||||
privateKey: privateKey,
|
||||
publicKey: publicKey,
|
||||
deviceID: buildDeviceID(publicKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAuthorizeURLAllowedOrigins(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -33,3 +36,20 @@ func TestParseAuthorizeURLRejectsUnknownOrigin(t *testing.T) {
|
||||
t.Fatal("parseAuthorizeURL() accepted an unknown origin")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthorizationCanonicalUsesDeviceCodeOnly(t *testing.T) {
|
||||
id := identity{deviceID: "D1-ABCD-EFGH-JKLM-NPQR"}
|
||||
target := authorizeTarget{
|
||||
App: "order_site",
|
||||
ChallengeID: "challenge-id",
|
||||
Challenge: "challenge-value",
|
||||
}
|
||||
got := buildAuthorizationCanonical(id, target, 1700000000)
|
||||
want := "dcauth.v2\norder_site\nchallenge-id\nchallenge-value\nD1-ABCD-EFGH-JKLM-NPQR\n1700000000"
|
||||
if got != want {
|
||||
t.Fatalf("canonical = %q, want %q", got, want)
|
||||
}
|
||||
if strings.Contains(got, "M1-") || strings.Contains(strings.ToLower(got), "machine") {
|
||||
t.Fatalf("canonical still contains machine code data: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,11 +141,10 @@ func handleDarwinAuthorization(identity identity, raw string) {
|
||||
if !confirmDarwin(
|
||||
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
|
||||
fmt.Sprintf(
|
||||
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n机器码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。",
|
||||
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。",
|
||||
app.DisplayName,
|
||||
clientVersion,
|
||||
identity.deviceID,
|
||||
identity.machineCode,
|
||||
),
|
||||
) {
|
||||
return
|
||||
@@ -158,14 +157,13 @@ func handleDarwinAuthorization(identity identity, raw string) {
|
||||
}
|
||||
|
||||
func showDarwinDeviceInfo(identity identity) {
|
||||
copyToClipboard(identity.deviceID + "\n" + identity.machineCode)
|
||||
copyToClipboard(identity.deviceID)
|
||||
showInfo(
|
||||
"DCAuth v"+clientVersion+" 设备信息",
|
||||
fmt.Sprintf(
|
||||
"版本:v%s\n\n设备码:%s\n机器码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码和机器码已复制到剪贴板。",
|
||||
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
|
||||
clientVersion,
|
||||
identity.deviceID,
|
||||
identity.machineCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,13 +16,12 @@ func runClient(protocolArg string) {
|
||||
|
||||
if protocolArg == "" {
|
||||
text := fmt.Sprintf(
|
||||
"DCAuth v%s\n设备码:%s\n机器码:%s\n\n设备码用于识别本机,机器码用于管理员核对。\n请从后台登录页点击“验证本机”完成登录。",
|
||||
"DCAuth v%s\n设备码:%s\n\n设备码用于识别本机设备。\n请从后台登录页点击“验证本机”完成登录。",
|
||||
clientVersion,
|
||||
identity.deviceID,
|
||||
identity.machineCode,
|
||||
)
|
||||
copyToClipboard(identity.deviceID + "\n" + identity.machineCode)
|
||||
showInfo("DCAuth v"+clientVersion+" 设备信息", text+"\n\n设备码和机器码已复制到剪贴板。")
|
||||
copyToClipboard(identity.deviceID)
|
||||
showInfo("DCAuth v"+clientVersion+" 设备信息", text+"\n\n设备码已复制到剪贴板。")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,7 +33,7 @@ func runClient(protocolArg string) {
|
||||
app := appRegistry[target.App]
|
||||
if !confirm(
|
||||
app.DisplayName+" · 本机登录确认",
|
||||
fmt.Sprintf("是否确认使用本机设备登录「%s」?\n\n设备码:%s\n机器码:%s", app.DisplayName, identity.deviceID, identity.machineCode),
|
||||
fmt.Sprintf("是否确认使用本机设备登录「%s」?\n\n设备码:%s", app.DisplayName, identity.deviceID),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
+46
-25
@@ -191,6 +191,7 @@ var (
|
||||
trayData notifyIconData
|
||||
trayAdded bool
|
||||
runtimeIdentity identity
|
||||
identityReady bool
|
||||
authorizationActive bool
|
||||
windowProcedure = syscall.NewCallback(dcauthWindowProc)
|
||||
)
|
||||
@@ -209,16 +210,14 @@ func runClient(protocolArg string) {
|
||||
if forwardToRunningInstance(protocolArg) {
|
||||
return
|
||||
}
|
||||
showError("DCAuth 启动失败", "已有 DCAuth 实例正在启动,但暂时无法连接,请稍后重试。")
|
||||
return
|
||||
firstInstance, err = acquireSingleInstance()
|
||||
if err != nil || !firstInstance {
|
||||
showError("DCAuth 启动失败", "DCAuth 已在运行但暂时没有响应。请在任务管理器中结束 DCAuth.exe 后重试。")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
registerProtocol()
|
||||
runtimeIdentity, err = loadOrCreateIdentity()
|
||||
if err != nil {
|
||||
showError("DCAuth 初始化失败", err.Error())
|
||||
return
|
||||
}
|
||||
if err := createHiddenWindow(); err != nil {
|
||||
showError("DCAuth 启动失败", err.Error())
|
||||
return
|
||||
@@ -228,10 +227,20 @@ func runClient(protocolArg string) {
|
||||
return
|
||||
}
|
||||
|
||||
runtimeIdentity, err = loadOrCreateIdentity()
|
||||
if err != nil {
|
||||
removeTrayIcon()
|
||||
showError("DCAuth 初始化失败", err.Error())
|
||||
destroyWindow.Call(mainWindow)
|
||||
mainWindow = 0
|
||||
return
|
||||
}
|
||||
identityReady = true
|
||||
|
||||
if protocolArg != "" {
|
||||
handleAuthorization(protocolArg)
|
||||
} else {
|
||||
showTrayBalloon("DCAuth v"+clientVersion+" 已在后台运行", "双击托盘图标可查看版本、设备码和机器码。")
|
||||
showTrayBalloon("DCAuth v"+clientVersion+" 已在后台运行", "双击托盘图标可查看版本和设备码。")
|
||||
}
|
||||
|
||||
runMessageLoop()
|
||||
@@ -279,16 +288,16 @@ func acquireNamedMutex(name string) (uintptr, bool, error) {
|
||||
}
|
||||
|
||||
func forwardToRunningInstance(protocolArg string) bool {
|
||||
for attempt := 0; attempt < 100; attempt++ {
|
||||
if postToRunningInstance(protocolArg) {
|
||||
return true
|
||||
for attempt := 0; attempt < 300; attempt++ {
|
||||
if hwnd := findRunningInstanceWindow(); hwnd != 0 {
|
||||
return sendToRunningInstance(hwnd, protocolArg)
|
||||
}
|
||||
sleep.Call(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func postToRunningInstance(protocolArg string) bool {
|
||||
func findRunningInstanceWindow() uintptr {
|
||||
classPtr, _ := syscall.UTF16PtrFromString(windowClassName)
|
||||
titlePtr, _ := syscall.UTF16PtrFromString(windowTitle)
|
||||
hwnd, _, _ := findWindowW.Call(
|
||||
@@ -297,12 +306,13 @@ func postToRunningInstance(protocolArg string) bool {
|
||||
)
|
||||
runtime.KeepAlive(classPtr)
|
||||
runtime.KeepAlive(titlePtr)
|
||||
if hwnd == 0 {
|
||||
return false
|
||||
}
|
||||
return hwnd
|
||||
}
|
||||
|
||||
func sendToRunningInstance(hwnd uintptr, protocolArg string) bool {
|
||||
if protocolArg == "" {
|
||||
postMessageW.Call(hwnd, showWindowMessage, 0, 0)
|
||||
return true
|
||||
result, _, _ := postMessageW.Call(hwnd, showWindowMessage, 0, 0)
|
||||
return result != 0
|
||||
}
|
||||
|
||||
encoded, _ := syscall.UTF16FromString(protocolArg)
|
||||
@@ -387,6 +397,9 @@ func runMessageLoop() {
|
||||
func dcauthWindowProc(hwnd, msg, wParam, lParam uintptr) uintptr {
|
||||
switch uint32(msg) {
|
||||
case wmCopyData:
|
||||
if !identityReady {
|
||||
return 0
|
||||
}
|
||||
data := (*copyDataStruct)(unsafe.Pointer(lParam))
|
||||
if data != nil && data.DwData == copyDataMagic && data.CbData > 0 && data.LpData != 0 {
|
||||
count := int(data.CbData / 2)
|
||||
@@ -396,12 +409,18 @@ func dcauthWindowProc(hwnd, msg, wParam, lParam uintptr) uintptr {
|
||||
}
|
||||
return 0
|
||||
case showWindowMessage:
|
||||
showDeviceInfo()
|
||||
if identityReady {
|
||||
showDeviceInfo()
|
||||
}
|
||||
return 0
|
||||
case trayCallbackMessage:
|
||||
switch uint32(lParam) {
|
||||
case wmLButtonDblClk:
|
||||
showDeviceInfo()
|
||||
if identityReady {
|
||||
showDeviceInfo()
|
||||
} else {
|
||||
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
|
||||
}
|
||||
case wmRButtonUp, wmContextMenu:
|
||||
showTrayMenu(hwnd)
|
||||
}
|
||||
@@ -497,7 +516,11 @@ func showTrayMenu(hwnd uintptr) {
|
||||
|
||||
switch command {
|
||||
case trayMenuDeviceInfo:
|
||||
showDeviceInfo()
|
||||
if identityReady {
|
||||
showDeviceInfo()
|
||||
} else {
|
||||
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
|
||||
}
|
||||
case trayMenuExit:
|
||||
destroyWindow.Call(hwnd)
|
||||
}
|
||||
@@ -505,12 +528,11 @@ func showTrayMenu(hwnd uintptr) {
|
||||
|
||||
func showDeviceInfo() {
|
||||
text := fmt.Sprintf(
|
||||
"版本:v%s\n\n设备码:%s\n机器码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码和机器码已复制到剪贴板。",
|
||||
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
|
||||
clientVersion,
|
||||
runtimeIdentity.deviceID,
|
||||
runtimeIdentity.machineCode,
|
||||
)
|
||||
copyToClipboard(runtimeIdentity.deviceID + "\n" + runtimeIdentity.machineCode)
|
||||
copyToClipboard(runtimeIdentity.deviceID)
|
||||
showInfo("DCAuth v"+clientVersion+" 设备信息", text)
|
||||
}
|
||||
|
||||
@@ -543,11 +565,10 @@ func handleAuthorization(raw string) {
|
||||
if !confirm(
|
||||
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
|
||||
fmt.Sprintf(
|
||||
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n机器码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。",
|
||||
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。",
|
||||
app.DisplayName,
|
||||
clientVersion,
|
||||
runtimeIdentity.deviceID,
|
||||
runtimeIdentity.machineCode,
|
||||
),
|
||||
) {
|
||||
return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 1,1,0,0
|
||||
PRODUCTVERSION 1,1,0,0
|
||||
FILEVERSION 2,0,0,0
|
||||
PRODUCTVERSION 2,0,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS 0x40004L
|
||||
@@ -13,12 +13,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Order Site"
|
||||
VALUE "FileDescription", "DCAuth device authentication client"
|
||||
VALUE "FileVersion", "1.1.0.0"
|
||||
VALUE "FileVersion", "2.0.0.0"
|
||||
VALUE "InternalName", "DCAuth"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2026 Order Site"
|
||||
VALUE "OriginalFilename", "DCAuth.exe"
|
||||
VALUE "ProductName", "DCAuth"
|
||||
VALUE "ProductVersion", "1.1.0"
|
||||
VALUE "ProductVersion", "2.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user