Go 安全编程:防范常见漏洞
安全是软件开发中最容易被忽视,却又最重要的环节。你可能写出功能完美、性能卓越的代码,但只要一个安全漏洞,就可能让整个系统崩溃,甚至导致用户数据泄露。
今天,我们就来深入探讨 Go 语言中的安全编程实践,学习如何防范常见的安全漏洞。
输入验证:第一道防线
永远不要信任用户输入。这是安全编程的黄金法则。
基础验证
package main
import (
"errors"
"regexp"
"strings"
"unicode/utf8"
)
// ValidationError 验证错误
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return e.Field + ": " + e.Message
}
// Validator 输入验证器
type Validator struct {
errors []ValidationError
}
// NewValidator 创建验证器
func NewValidator() *Validator {
return &Validator{}
}
// Check 检查条件
func (v *Validator) Check(field string, condition bool, message string) {
if !condition {
v.errors = append(v.errors, ValidationError{
Field: field,
Message: message,
})
}
}
// HasErrors 是否有错误
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
// Errors 获取所有错误
func (v *Validator) Errors() []ValidationError {
return v.errors
}
// ValidateEmail 验证邮箱格式
func ValidateEmail(email string) error {
v := NewValidator()
email = strings.TrimSpace(email)
v.Check("email", email != "", "邮箱不能为空")
v.Check("email", utf8.RuneCountInString(email) <= 254, "邮箱长度不能超过 254 个字符")
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
v.Check("email", emailRegex.MatchString(email), "邮箱格式不正确")
if v.HasErrors() {
return errors.New(v.Errors()[0].Message)
}
return nil
}
// ValidatePassword 验证密码强度
func ValidatePassword(password string) error {
v := NewValidator()
v.Check("password", len(password) >= 8, "密码长度至少 8 位")
v.Check("password", len(password) <= 72, "密码长度不能超过 72 位")
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password)
v.Check("password", hasUpper, "密码必须包含大写字母")
v.Check("password", hasLower, "密码必须包含小写字母")
v.Check("password", hasNumber, "密码必须包含数字")
v.Check("password", hasSpecial, "密码必须包含特殊字符")
if v.HasErrors() {
return errors.New(v.Errors()[0].Message)
}
return nil
}
func main() {
// 测试邮箱验证
if err := ValidateEmail("user@example.com"); err != nil {
println("邮箱验证失败:", err.Error())
}
// 测试密码验证
if err := ValidatePassword("MyPass123!"); err != nil {
println("密码验证失败:", err.Error())
}
}
防止路径遍历攻击
package main
import (
"errors"
"path/filepath"
"strings"
)
// SafeFileAccess 安全的文件访问
func SafeFileAccess(basePath, userPath string) (string, error) {
// 清理路径
cleanPath := filepath.Clean(userPath)
// 检查是否包含可疑字符
if strings.Contains(cleanPath, "..") {
return "", errors.New("invalid path: contains '..'")
}
// 构建完整路径
fullPath := filepath.Join(basePath, cleanPath)
// 确保最终路径在基础目录内
absBase, _ := filepath.Abs(basePath)
absFull, _ := filepath.Abs(fullPath)
if !strings.HasPrefix(absFull, absBase) {
return "", errors.New("access denied: path outside base directory")
}
return absFull, nil
}
func main() {
basePath := "/var/www/uploads"
// 安全的路径
path1, err := SafeFileAccess(basePath, "images/avatar.jpg")
println(path1, err)
// 危险的路径(路径遍历攻击)
path2, err := SafeFileAccess(basePath, "../../etc/passwd")
println(path2, err)
}
SQL 注入防护
SQL 注入是最常见也最危险的攻击之一。让我们看看如何在 Go 中防范。
错误的做法
// ❌ 危险:直接拼接 SQL
func getUserByIDUnsafe(db *sql.DB, id string) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = " + id
row := db.QueryRow(query)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
return &user, err
}
// 攻击者可以传入: 1 OR 1=1
// 结果: SELECT id, name, email FROM users WHERE id = 1 OR 1=1
// 这会返回所有用户!
正确的做法
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
// User 用户结构
type User struct {
ID int64
Name string
Email string
}
// UserRepository 用户数据访问
type UserRepository struct {
db *sql.DB
}
// NewUserRepository 创建用户仓库
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// GetByID 通过 ID 查询用户(安全)
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*User, error) {
// ✅ 使用参数化查询
query := "SELECT id, name, email FROM users WHERE id = $1"
row := r.db.QueryRowContext(ctx, query, id)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &user, nil
}
// Search 搜索用户(安全)
func (r *UserRepository) Search(ctx context.Context, keyword string) ([]User, error) {
// ✅ 使用参数化查询
query := "SELECT id, name, email FROM users WHERE name LIKE $1 OR email LIKE $1"
rows, err := r.db.QueryContext(ctx, query, "%"+keyword+"%")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
// BatchGet 批量查询用户(安全)
func (r *UserRepository) BatchGet(ctx context.Context, ids []int64) ([]User, error) {
if len(ids) == 0 {
return nil, nil
}
// ✅ 动态构建参数占位符
query := "SELECT id, name, email FROM users WHERE id = ANY($1)"
// PostgreSQL 支持数组参数
rows, err := r.db.QueryContext(ctx, query, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
repo := NewUserRepository(db)
// 安全的查询
user, err := repo.GetByID(context.Background(), 1)
if err != nil {
panic(err)
}
fmt.Printf("User: %+v\n", user)
}
XSS 防护(跨站脚本攻击)
XSS 攻击允许攻击者在用户的浏览器中执行恶意脚本。
HTML 转义
package main
import (
"html/template"
"net/http"
"strings"
)
// SanitizeInput 清理用户输入
func SanitizeInput(input string) string {
// 移除危险字符
input = strings.ReplaceAll(input, "<script", "<script")
input = strings.ReplaceAll(input, "</script", "</script")
input = strings.ReplaceAll(input, "<", "<")
input = strings.ReplaceAll(input, ">", ">")
input = strings.ReplaceAll(input, "\"", """)
input = strings.ReplaceAll(input, "'", "'")
return input
}
// SafeHandler 安全的 HTTP 处理器
func SafeHandler(w http.ResponseWriter, r *http.Request) {
userInput := r.URL.Query().Get("name")
// ✅ 方法 1: 使用 html/template 自动转义
tmpl := template.Must(template.New("page").Parse(`
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Hello, {{.Name}}!</h1>
</body>
</html>
`))
data := struct {
Name string
}{
Name: userInput, // template 会自动转义
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
// UnsafeHandler 不安全的 HTTP 处理器(反面教材)
func UnsafeHandler(w http.ResponseWriter, r *http.Request) {
userInput := r.URL.Query().Get("name")
// ❌ 危险:直接输出用户输入
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Hello, %s!</h1>
</body>
</html>
`, userInput)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
// 攻击者可以传入: <script>alert('XSS')</script>
}
func main() {
http.HandleFunc("/safe", SafeHandler)
http.HandleFunc("/unsafe", UnsafeHandler)
http.ListenAndServe(":8080", nil)
}
Content Security Policy
package main
import (
"net/http"
)
// CSPMiddleware 添加 Content Security Policy 头
func CSPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置严格的 CSP
csp := "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://trusted.cdn.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'"
w.Header().Set("Content-Security-Policy", csp)
// 其他安全头
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
// 应用 CSP 中间件
http.ListenAndServe(":8080", CSPMiddleware(mux))
}
CSRF 防护(跨站请求伪造)
CSRF 攻击利用用户已登录的身份,在用户不知情的情况下执行恶意请求。
CSRF Token 实现
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"sync"
"time"
)
// CSRFToken CSRF 令牌
type CSRFToken struct {
Token string
ExpiresAt time.Time
}
// CSRFStore CSRF 令牌存储
type CSRFStore struct {
mu sync.RWMutex
tokens map[string]CSRFToken
}
// NewCSRFStore 创建 CSRF 存储
func NewCSRFStore() *CSRFStore {
return &CSRFStore{
tokens: make(map[string]CSRFToken),
}
}
// GenerateToken 生成新的 CSRF 令牌
func (s *CSRFStore) GenerateToken(sessionID string) (string, error) {
// 生成随机令牌
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
token := base64.URLEncoding.EncodeToString(bytes)
// 存储令牌
s.mu.Lock()
s.tokens[sessionID] = CSRFToken{
Token: token,
ExpiresAt: time.Now().Add(1 * time.Hour),
}
s.mu.Unlock()
return token, nil
}
// ValidateToken 验证 CSRF 令牌
func (s *CSRFStore) ValidateToken(sessionID, token string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
stored, exists := s.tokens[sessionID]
if !exists {
return false
}
// 检查是否过期
if time.Now().After(stored.ExpiresAt) {
return false
}
// 比较令牌
return stored.Token == token
}
var csrfStore = NewCSRFStore()
// FormHandler 处理表单显示
func FormHandler(w http.ResponseWriter, r *http.Request) {
// 从 session 获取 sessionID(这里简化处理)
sessionID := "user-session-123"
// 生成 CSRF 令牌
token, err := csrfStore.GenerateToken(sessionID)
if err != nil {
http.Error(w, "Failed to generate token", 500)
return
}
// 将令牌嵌入表单
tmpl := template.Must(template.New("form").Parse(`
<!DOCTYPE html>
<html>
<head><title>Transfer Money</title></head>
<body>
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{.Token}}">
<label>To Account: <input type="text" name="to_account"></label><br>
<label>Amount: <input type="number" name="amount"></label><br>
<button type="submit">Transfer</button>
</form>
</body>
</html>
`))
tmpl.Execute(w, struct{ Token string }{Token: token})
}
// TransferHandler 处理转账请求
func TransferHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", 405)
return
}
sessionID := "user-session-123"
token := r.FormValue("csrf_token")
// ✅ 验证 CSRF 令牌
if !csrfStore.ValidateToken(sessionID, token) {
http.Error(w, "Invalid CSRF token", 403)
return
}
// 处理转账逻辑
toAccount := r.FormValue("to_account")
amount := r.FormValue("amount")
fmt.Fprintf(w, "Transferred %s to account %s", amount, toAccount)
}
func main() {
http.HandleFunc("/form", FormHandler)
http.HandleFunc("/transfer", TransferHandler)
http.ListenAndServe(":8080", nil)
}
密码安全存储
永远不要明文存储密码!
使用 bcrypt 加密
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 加密密码
func HashPassword(password string) (string, error) {
// 使用默认成本因子 10
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "MySecurePassword123!"
// 加密密码
hash, err := HashPassword(password)
if err != nil {
panic(err)
}
fmt.Println("Hashed password:", hash)
// 验证正确密码
if CheckPassword(password, hash) {
fmt.Println("✅ Password is correct")
}
// 验证错误密码
if !CheckPassword("WrongPassword", hash) {
fmt.Println("❌ Password is incorrect")
}
}
完整的用户认证系统
package main
import (
"context"
"database/sql"
"errors"
"golang.org/x/crypto/bcrypt"
"time"
)
// User 用户结构
type User struct {
ID int64
Email string
PasswordHash string
CreatedAt time.Time
}
// AuthRepository 认证数据访问
type AuthRepository struct {
db *sql.DB
}
// NewAuthRepository 创建认证仓库
func NewAuthRepository(db *sql.DB) *AuthRepository {
return &AuthRepository{db: db}
}
// CreateUser 创建用户(密码加密)
func (r *AuthRepository) CreateUser(ctx context.Context, email, password string) (*User, error) {
// 加密密码
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 插入用户
query := "INSERT INTO users (email, password_hash, created_at) VALUES ($1, $2, $3) RETURNING id"
var id int64
err = r.db.QueryRowContext(ctx, query, email, string(hash), time.Now()).Scan(&id)
if err != nil {
return nil, err
}
return &User{
ID: id,
Email: email,
PasswordHash: string(hash),
}, nil
}
// Authenticate 用户认证
func (r *AuthRepository) Authenticate(ctx context.Context, email, password string) (*User, error) {
// 查询用户
query := "SELECT id, email, password_hash, created_at FROM users WHERE email = $1"
row := r.db.QueryRowContext(ctx, query, email)
var user User
err := row.Scan(&user.ID, &user.Email, &user.PasswordHash, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.New("invalid email or password")
}
return nil, err
}
// 验证密码
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
return nil, errors.New("invalid email or password")
}
return &user, nil
}
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
repo := NewAuthRepository(db)
// 创建用户
user, err := repo.CreateUser(context.Background(), "user@example.com", "SecurePass123!")
if err != nil {
panic(err)
}
fmt.Printf("Created user: %+v\n", user)
// 认证用户
authedUser, err := repo.Authenticate(context.Background(), "user@example.com", "SecurePass123!")
if err != nil {
panic(err)
}
fmt.Printf("Authenticated user: %+v\n", authedUser)
}
JWT 安全实践
JWT(JSON Web Token)常用于 API 认证,但使用不当会带来安全风险。
安全的 JWT 实现
package main
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"time"
)
// Claims JWT 声明
type Claims struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// JWTManager JWT 管理器
type JWTManager struct {
secretKey []byte
tokenDuration time.Duration
}
// NewJWTManager 创建 JWT 管理器
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
return &JWTManager{
secretKey: []byte(secretKey),
tokenDuration: tokenDuration,
}
}
// GenerateToken 生成 JWT 令牌
func (m *JWTManager) GenerateToken(userID int64, email string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.tokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(m.secretKey)
}
// ValidateToken 验证 JWT 令牌
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// ✅ 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return m.secretKey, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
// ✅ 验证发行者
if claims.Issuer != "myapp" {
return nil, errors.New("invalid issuer")
}
return claims, nil
}
func main() {
// 使用强密钥(至少 32 字节)
secretKey := "your-256-bit-secret-key-here-1234567890"
manager := NewJWTManager(secretKey, 24*time.Hour)
// 生成令牌
token, err := manager.GenerateToken(123, "user@example.com")
if err != nil {
panic(err)
}
fmt.Println("Generated token:", token)
// 验证令牌
claims, err := manager.ValidateToken(token)
if err != nil {
panic(err)
}
fmt.Printf("Token claims: UserID=%d, Email=%s\n", claims.UserID, claims.Email)
}
JWT 中间件
package main
import (
"context"
"net/http"
"strings"
)
type contextKey string
const userContextKey contextKey = "user"
// AuthMiddleware JWT 认证中间件
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 Authorization 头获取令牌
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", 401)
return
}
// 验证 Bearer 格式
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", 401)
return
}
tokenString := parts[1]
// 验证令牌
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
http.Error(w, "Invalid or expired token", 401)
return
}
// 将用户信息存入上下文
ctx := context.WithValue(r.Context(), userContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ProtectedHandler 受保护的处理器
func ProtectedHandler(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(userContextKey).(*Claims)
fmt.Fprintf(w, "Hello, user %d (%s)!", claims.UserID, claims.Email)
}
func main() {
jwtManager := NewJWTManager("your-secret-key", 24*time.Hour)
mux := http.NewServeMux()
mux.Handle("/protected", AuthMiddleware(jwtManager)(http.HandlerFunc(ProtectedHandler)))
http.ListenAndServe(":8080", mux)
}
依赖安全审计
第三方依赖可能包含安全漏洞。定期审计依赖是必要的。
使用 govulncheck
# 安装 govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest
# 扫描项目依赖
govulncheck ./...
# 输出示例:
# Scanning your dependencies...
# Your code is affected by 1 vulnerability that we found in your imported symbols.
#
# CVE-2023-XXXXX: Packages
# - github.com/vulnerable/package@v1.0.0
# Fixed in: v1.0.1
# More info: https://pkg.go.dev/vuln/GO-2023-XXXX
自动化依赖更新
# .github/workflows/dependency-review.yml
name: Dependency Review
on:
pull_request:
paths:
- 'go.mod'
- 'go.sum'
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: govulncheck ./...
- name: Dependency Review
uses: actions/dependency-review-action@v3
with:
fail-on-severity: high
TLS 配置
确保 HTTPS 配置安全:
package main
import (
"crypto/tls"
"net/http"
"time"
)
func main() {
// ✅ 安全的 TLS 配置
tlsConfig := &tls.Config{
// 最低 TLS 1.2
MinVersion: tls.VersionTLS12,
// 禁用弱密码套件
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
// 启用 HSTS
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
// 优先使用服务器密码套件
PreferServerCipherSuites: true,
}
server := &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 启动 HTTPS 服务器
server.ListenAndServeTLS("cert.pem", "key.pem")
}
总结
安全编程是一个持续的过程。今天我们学习了:
核心原则:
- 输入验证:永远不信任用户输入
- 参数化查询:防止 SQL 注入
- 输出转义:防止 XSS 攻击
- CSRF 令牌:防止跨站请求伪造
最佳实践:
- 密码加密:使用 bcrypt,永远不明文存储
- JWT 安全:验证签名、过期时间和发行者
- 依赖审计:定期扫描第三方库漏洞
- TLS 配置:使用强密码套件和最新协议
记住,安全不是一次性的工作,而是需要持续关注和改进的过程。定期更新依赖、进行安全审计、关注安全公告,这些都是保护应用安全的重要环节。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。