这篇文章上次修改于 903 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
1. SSH Key 免密登录远程服务器
通过 SSH Key 免密登录远程服务器可避免服务器密码被人暴力破解。
操作方式详见:给远程服务器设置SSH Key免密码登录
2. HTTPS
HTTPS 协议是由 HTTP 加上 TLS/SSL 协议构建的可进行加密传输、身份认证的网络协议,主要通过数字证书、加密算法、非对称密钥等技术完成互联网数据传输加密,实现互联网传输安全保护。
申请证书及配置 Nginx 步骤如下。
申请 SSL 证书:
yum -y -q install yum-utils
yum-config-manager --enable rhui-REGION-rhel-server-extras rhui-REGION-rhel-server-optional
yum -y -q install certbot
certbot certonly --standalone -n -m ${EMAIL} --agree-tos -d ${DOMAIN_NAME}
证书自动续期:
yum install python-certbot-nginx -y
sed -i "s/renew/renew --nginx/g" /usr/lib/systemd/system/certbot-renew.service
systemctl daemon-reload
systemctl start certbot-renew.service
上述申请的证书位于 /etc/letsencrypt/live/${your_domain} 下,用该证书配置 Nginx:
server {
listen 443 ssl default_server;
server_name aping-dev.com;
server_tokens off;
keepalive_timeout 5;
# 证书
ssl_certificate "/etc/letsencrypt/live/${your_domain}/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/${your_domain}/privkey.pem";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
root /usr/local/lighthouse/softwares/nginx/html;;
index index.php index.html;
# 日志
access_log logs/blog.log combinediox;
error_log logs/blog.error.log;
# 后端
location ~* ^/back/(.*) {
rewrite ^/back(.*)$ $1 break;
# 支持 websocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:9000;
}
# 前端
location / {
# 支持 websocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:8080;
}
}
# http 转 https
server {
listen 80;
server_name ${your_domain};
return 301 https://$host$request_uri;
}
3. JWT
JWT 用来对用户进行认证,可防止用户的数据被别的用户篡改。
3.1 原理
jwt 认证过程
登录
- 前端将用户名、密码传给后端。
- 后端验证其合法后生成 token,返回给前端。注意,每次生成 token 时,都应该使用新的随机生成的 secret key。
- 前端记录下 token。
请求
- 前端向后端发出请求时将 token 放在 header 中。
- 后端对 header 中的 token 进行验证,如果正确且没有超时,则将请求结果返回给前端,否则将错误返回给前端。
- 前端收到返回后,如果发现错误,则重新跳转到登录页面。
详见
3.2 实现
3.2.1 后端
package utils
import (
"errors"
"fmt"
"math/rand"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
type UserClaim struct {
jwt.StandardClaims
// 要加密的字段
Id int64
Username string
}
// 后端收到登录请求后,生成 token
func GenerateToken(id int64, username, secretKey string, expireSeconds int64) (string, error) {
claims := &UserClaim{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Duration(expireSeconds) * time.Second).Unix(),
},
Id: id,
Username: username,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secretKey))
}
// 后端收到非登录请求后,解析 token,如果 token 过期会报错
func ParseToken(tokenStr, secretKey string) (*UserClaim, error) {
token, err := jwt.ParseWithClaims(tokenStr, &UserClaim{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("invalid signature method: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*UserClaim); ok && token.Valid {
return claims, nil
} else {
return nil, errors.New("user unauthorized")
}
}
// 每次生成 token 时,都应该用新生成的随机 secret key
func GenSecretKey(length int) string {
result := ""
characters := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
chaLen := len(characters)
for i := 0; i < length; i++ {
result += string(characters[rand.Intn(chaLen)])
}
return result
}
后端收到非登录请求后,验证 token
func (bc *BaseController) VerifyToken(context *ApiContext) error {
// 从 header 中获取 token 并解析,如果 token 过期会报错
token := context.Request().Header.Get("X-Token")
userClaim, err := utils.ParseToken(token, conf.LoginSecretKey)
if err != nil {
err = errors.New("login expired")
context.ApiData.Err = goerror.New(errno.EUserUnauthorized, err.Error())
return err
}
// 从 db 中获取用户
userSvc := user.NewSvc(context.TraceId)
user, err := userSvc.GetByUsername(conf.AdminName)
if err != nil {
context.ApiData.Err = goerror.New(errno.EUserUnauthorized, err.Error())
return err
}
// 如果从 token 中解析出的用户与 db 中的不符,则报错
if userClaim.Id != user.Id || userClaim.Username != user.Username {
err = errors.New("invalid user")
context.ApiData.Err = goerror.New(errno.EUserUnauthorized, err.Error())
return err
}
return nil
}
3.2.2 前端
import axios from 'axios'
import {Code} from '../const/code.js'
import {getToken, removeToken} from '../utils/auth.js'
// 创建 axios 实例
const service = axios.create({
baseURL: '', // api 的 base_url
timeout: 5000 // 请求超时时间
})
// request 拦截器
service.interceptors.request.use(
config => {
if (getToken() != '') {
config.headers['X-Token'] = getToken() // 让每个请求携带 token
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code != Code.EUserUnauthorized) { // 如果后端返回 token 非法
return response.data
} else {
MessageBox.confirm(
'登录已过期,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
removeToken();
location.reload() // 为了重新实例化 vue-router 对象 避免 bug
})
return Promise.reject('error')
}
},
error => {
// 出现网络超时
router.push('500')
return Promise.reject(error)
}
)
export default service
4. 密码加密存储
4.1 管理员密码存储
存储管理员密码时使用 MD5 加密,该加密算法是不可逆的。验证密码是否正确时,将请求中的密码采用 md5 加密后与存储的密码进行比较。
4.2 配置文件中的密码存储
比如 MySql 的密码存储采用 AES 加密,该加密算法是可逆的,使用的时候解密即可。
5. 防 SQL 注入
对输入的参数进行校验,包括类型校验和格式校验。
6. API 限频
如果用户登录了,通过用户 ID 限频;否则,通过真实 IP 限频。
7. 定期备份
定期备份 DB 中的数据。
8. 防火墙设置
只开放必要的端口。
没有评论