feat: 准备 Admin 线上 HTTPS 部署 (#81)
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=CM AutoBuy Admin
|
||||||
|
After=network-online.target mysql84.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=mysql84.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=cmautobuy
|
||||||
|
Group=cmautobuy
|
||||||
|
WorkingDirectory=/opt/cmautobuy
|
||||||
|
EnvironmentFile=/etc/cmautobuy/admin.env
|
||||||
|
ExecStart=/opt/cmautobuy/cmautobuy-admin -addr 127.0.0.1:18080
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
TimeoutStopSec=30s
|
||||||
|
UMask=0077
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/opt/cmautobuy/data
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 将域名和证书路径替换为实际值后放入服务器 Nginx 配置目录。
|
||||||
|
# Admin 本身只监听 127.0.0.1:18080,不直接开放公网端口。
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name admin.example.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name admin.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/fullchain.pem;
|
||||||
|
ssl_certificate_key /path/to/privkey.pem;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:18080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,7 +202,7 @@ func safeNext(raw string) string {
|
|||||||
func setAuthCookie(c *gin.Context, token string, expiresAt time.Time) {
|
func setAuthCookie(c *gin.Context, token string, expiresAt time.Time) {
|
||||||
http.SetCookie(c.Writer, &http.Cookie{
|
http.SetCookie(c.Writer, &http.Cookie{
|
||||||
Name: authCookieName, Value: token, Path: "/", HttpOnly: true,
|
Name: authCookieName, Value: token, Path: "/", HttpOnly: true,
|
||||||
Secure: c.Request.TLS != nil, SameSite: http.SameSiteLaxMode,
|
Secure: requestIsHTTPS(c.Request), SameSite: http.SameSiteLaxMode,
|
||||||
Expires: expiresAt, MaxAge: int(service.WebSessionDuration.Seconds()),
|
Expires: expiresAt, MaxAge: int(service.WebSessionDuration.Seconds()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@ func setAuthCookie(c *gin.Context, token string, expiresAt time.Time) {
|
|||||||
func clearAuthCookie(c *gin.Context) {
|
func clearAuthCookie(c *gin.Context) {
|
||||||
http.SetCookie(c.Writer, &http.Cookie{
|
http.SetCookie(c.Writer, &http.Cookie{
|
||||||
Name: authCookieName, Value: "", Path: "/", HttpOnly: true,
|
Name: authCookieName, Value: "", Path: "/", HttpOnly: true,
|
||||||
Secure: c.Request.TLS != nil, SameSite: http.SameSiteLaxMode,
|
Secure: requestIsHTTPS(c.Request), SameSite: http.SameSiteLaxMode,
|
||||||
Expires: time.Unix(1, 0), MaxAge: -1,
|
Expires: time.Unix(1, 0), MaxAge: -1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,14 @@ func TestAuthCookie安全属性(t *testing.T) {
|
|||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
tls bool
|
tls bool
|
||||||
|
remoteAddress string
|
||||||
|
forwarded string
|
||||||
secure bool
|
secure bool
|
||||||
}{
|
}{
|
||||||
{"HTTP", false, false},
|
{name: "HTTP", secure: false},
|
||||||
{"HTTPS", true, true},
|
{name: "HTTPS", tls: true, secure: true},
|
||||||
|
{name: "本机HTTPS反向代理", remoteAddress: "127.0.0.1:12345", forwarded: "https", secure: true},
|
||||||
|
{name: "外部来源不能伪造代理头", remoteAddress: "203.0.113.8:12345", forwarded: "https", secure: false},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
@@ -53,6 +57,12 @@ func TestAuthCookie安全属性(t *testing.T) {
|
|||||||
if test.tls {
|
if test.tls {
|
||||||
request.TLS = &tls.ConnectionState{}
|
request.TLS = &tls.ConnectionState{}
|
||||||
}
|
}
|
||||||
|
if test.remoteAddress != "" {
|
||||||
|
request.RemoteAddr = test.remoteAddress
|
||||||
|
}
|
||||||
|
if test.forwarded != "" {
|
||||||
|
request.Header.Set("X-Forwarded-Proto", test.forwarded)
|
||||||
|
}
|
||||||
context, _ := gin.CreateTestContext(response)
|
context, _ := gin.CreateTestContext(response)
|
||||||
context.Request = request
|
context.Request = request
|
||||||
setAuthCookie(context, "raw-token", time.Now().Add(12*time.Hour))
|
setAuthCookie(context, "raw-token", time.Now().Add(12*time.Hour))
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ func CSRFMiddleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// HttpOnly 必须为 false —— 双提交方案要让页面把值填进表单。
|
// HttpOnly 必须为 false —— 双提交方案要让页面把值填进表单。
|
||||||
// 本项目只监听本机,Secure 先留 false,将来上 HTTPS 再打开。
|
http.SetCookie(c.Writer, &http.Cookie{
|
||||||
c.SetCookie(csrfCookieName, token, 12*3600, "/", "", false, false)
|
Name: csrfCookieName, Value: token, Path: "/", HttpOnly: false,
|
||||||
|
Secure: requestIsHTTPS(c.Request), SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: 12 * 3600,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// 交给模板渲染成隐藏字段
|
// 交给模板渲染成隐藏字段
|
||||||
c.Set(csrfFieldName, token)
|
c.Set(csrfFieldName, token)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// requestIsHTTPS 识别直连 TLS,或仅信任本机反向代理传入的 HTTPS 标记。
|
||||||
|
// Admin 生产进程只监听 127.0.0.1,Nginx 在同机终止 TLS;不能无条件信任
|
||||||
|
// X-Forwarded-Proto,否则直接访问端口的客户端可以伪造安全来源。
|
||||||
|
func requestIsHTTPS(request *http.Request) bool {
|
||||||
|
if request.TLS != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(request.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
remoteIP := net.ParseIP(host)
|
||||||
|
if remoteIP == nil || !remoteIP.IsLoopback() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
forwardedProto := strings.TrimSpace(strings.Split(request.Header.Get("X-Forwarded-Proto"), ",")[0])
|
||||||
|
return strings.EqualFold(forwardedProto, "https")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user