系统安全及漏洞扫描专题

2/2/2022 Spring-SecurityNessus-ScannerRSAAES加密算法漏洞扫描登录认证日志审计系统安全

# 1. Nessus Scanner扫描安全漏洞

# 1.1 Nessus Scanner简介

Nessus号称是世界上最流行的漏洞扫描程序,全世界有超过75000个组织在使用它。该工具提供完整的电脑漏洞扫描服务,并随时更新其漏洞数据库。Nessus不同于传统的漏洞扫描软件,Nessus可同时在本机或远端上遥控,进行系统的漏洞分析扫描。

Nessus

# 1.2 搭建破解版Nessus Scanner服务

# 1.2.1 搭建服务

官方的Nessus Scanner需要破解,我这里在Github上找了一个docker部署的免破解项目。

项目地址:https://github.com/elliot-bia/nessus (opens new window)

$ docker run -itd --name nessus -p 8834:8834 --restart=always ramisec/nessus
1

这时候我们用http访问服务地址,发现是被限制访问的。

Nessus禁止HTTP访问

下面我们有两种方式去访问。

  • 方法一:直接把http改成https(临时方案)

    直接把http改成https,但由于证书是无效的,Chrome会爆出“您的连接不是私密连接”的错误,这时候在旁边空白处鼠标左键单击一下,然后输入thisisunsafe,即可刷新成功访问。

  • 方法二:配置域名申请证书并开启https(永久方案)

    配置过程这里就不赘述了,详见我的另一篇博客:VPS基本部署环境的搭建与配置 (opens new window),需要注意的一点是,配置proxy_pass的时候,要写https地址。

# 1.2.2 重置密码

现在可以访问页面了,但登录密码处作者留了个彩蛋,没有公开,真的调皮。

nessus密码彩蛋

这里我们也不去破解了,直接去重置密码,然后用新密码登录即可。

$ docker exec -it nessus /bin/bash   # 进入容器内部
$ cd /opt/nessus/sbin/               # 切换到到Nessus目录
$ ./nessuscli lsuser                 # 列出所有用户(默认只有admin)
$ ./nessuscli chpasswd admin         # 更新admin用户的密码,需要输入两次
1
2
3
4

注:如果重置一遍没生效的话,就再去重置一遍,不清楚为什么我重置了两次才生效。

# 1.2.3 破解更新

这时候,打开 Nessus Scanner,想要新建一个扫描任务,会出现500内部服务器错误,原因是我们还没有破解更新,执行如下命令即可。

$ docker exec -it nessus /bin/bash /nessus/update.sh
1

Nessus-Scanner破解更新

破解更新成功后,再去新建一个扫描任务,就可以正常显示了。

Nessus-Scanner新建扫描任务

# 1.3 Nessus Scanner基本使用

# 1.3.1 新建任务扫描安全漏洞

点击New Scan——选择Advanced Scan,其中Name随便填,Targets填你要扫描的主机,Description可填可不填。

Nessus-Scanner新建扫描任务

配置完毕后,在列表页点击三角符号开始扫描,会实时显示扫描出的安全漏洞,扫描耗时比较长,耐心等待扫描结束。

Nessus-Scanner扫描过程

# 1.3.2 查看漏洞报告并针对性修复

Vulnerabilities处可以查看到所有扫描出的安全漏洞列表,点开即可查看详情,会给出漏洞描述、漏洞修复建议、漏洞端口等相关描述信息,可以根据这个对安全漏洞进行针对性的修复。

Nessus-Scanner查看漏洞报告

# 1.4 公开的安全漏洞检索

[1] 漏洞情报中心:https://vip.riskivy.com/ (opens new window)

漏洞情报中心

[2] 国家信息安全漏洞库:https://www.cnnvd.org.cn/home/childHome (opens new window)

国家信息安全漏洞库

# 1.5 Log4J严重安全漏洞

2021年11月在 Apache Log4J 日志记录库发现的一个严重漏洞。它本质上可以让黑客完全控制运行未打补丁 Log4J 版本的设备。恶意行为者可以利用该漏洞在易受攻击的系统中运行自己想要的几乎任何代码。

研究人员认为它是个“灾难性”安全漏洞,因为 Log4J 是全球部署最广泛的开源程序之一,而且该漏洞很容易利用。美国网络安全和基础设施安全局主任称“这是我整个职业生涯中见过的最严重的漏洞之一,即使不是唯一”。虽然该漏洞在被发现后不久就得到修补,但仍会带来多年风险,因为 Log4J 已深植于软件供应链。

Log4J严重安全漏洞

# 2. 数据加密解密算法

# 2.1 加密解密算法概述

AES加密为最常见的对称加密算法,RSA加密为最常见的非对称加密算法。平时开发的时候使用这两种算法进行加密,不要使用MD5,这是个摘要算法,不是用来加密的,加不加盐也都早就可以被完全破解了。

# 2.1.1 对称加密与非对称加密

对称加密的通信双方使用相同的密钥,如果一方的密钥遭泄露,那么整个通信就会被破解。 而非对称加密使用一对密钥,一个用来加密,一个用来解密,而且公钥是公开的,秘钥是自己保存的,不需要像对称加密那样在通信之前要先同步密钥。 非对称加密相比于对称加密,其安全性更好。

# 2.1.2 AES对称加密算法

AES加密为最常见的对称加密算法(微信小程序的加密传输就是用的这个加密算法)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

AES加密传输流程

# 2.1.3 RSA非对称加密算法

RSA加密算法是一种非对称加密算法,所谓非对称,就是指该算法加密和解密使用不同的密钥,即使用加密密钥进行加密、解密密钥进行解密。在RSA算法中,加密密钥PK是公开信息,而解密密钥SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,由于无法计算出大数n的欧拉函数phi(N),所以不能根据PK计算出SK。

# 2.1.4 MD5摘要算法

MD5信息摘要算法,是一种被广泛使用的密码散列函数,可以产生出一个128位的散列值,用于确保信息传输完整一致。MD5由美国密码学家Ronald Linn Rivest设计,于1992年公开,用以取代MD4算法。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法。2004年,证实MD5算法无法防止碰撞,因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。

# 2.2 RSA加密解密

# 2.2.1 Java封装

import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA工具类
 */
public class RsaUtility {

    /**
     * 使用公钥字符串加密
     *
     * @param plainText       明文
     * @param publicKeyString 公钥字符串
     * @return 密文
     * @throws Exception 异常
     */
    public static String encryptByPublicString(String plainText, String publicKeyString) throws Exception {
        PublicKey publicKey = getPublicKeyByString(publicKeyString);
        return encrypt(plainText, publicKey);
    }

    /**
     * 使用私钥字符串解密
     *
     * @param cipherText       密文
     * @param privateKeyString 私钥字符串
     * @return 明文
     * @throws Exception 异常
     */
    public static String decryptByPrivateString(String cipherText, String privateKeyString) throws Exception {
        PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
        return decrypt(cipherText, privateKey);
    }

    /**
     * 使用私钥字符串签名
     *
     * @param plainText        明文
     * @param privateKeyString 私钥字符串
     * @return 签名
     * @throws Exception 异常
     */
    public static String signByPrivateString(String plainText, String privateKeyString) throws Exception {
        PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
        return sign(plainText, privateKey);
    }

    /**
     * 使用公钥字符串验签
     *
     * @param plainText       明文
     * @param signature       签名
     * @param publicKeyString 公钥字符串
     * @return 是否通过签名验证
     * @throws Exception 异常
     */
    public static boolean verifyByPublicString(String plainText, String signature, String publicKeyString) throws Exception {
        PublicKey publicKey = getPublicKeyByString(publicKeyString);
        return verify(plainText, signature, publicKey);
    }

    /**
     * 生成2048位的RSA密钥对
     *
     * @return 密钥对
     * @throws Exception 异常
     */
    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048, new SecureRandom());
        return generator.generateKeyPair();
    }

    /**
     * 从RSA密钥对中获取私钥字符串
     *
     * @param keyPair RSA密钥对
     * @return 私钥字符串
     */
    public static String getPrivateKeyString(KeyPair keyPair) {
        return getPrivateKeyString(keyPair.getPrivate());
    }

    public static String getPrivateKeyString(PrivateKey privateKey) {
        return Base64.encodeBase64String(privateKey.getEncoded());
    }

    /**
     * 从RSA密钥对中获取公钥字符串
     *
     * @param keyPair RSA密钥对
     * @return 公钥字符串
     */
    public static String getPublicKeyString(KeyPair keyPair) {
        return Base64.encodeBase64String(keyPair.getPublic().getEncoded());
    }

    /**
     * 将私钥字符串还原为私钥
     *
     * @param privateKeyString 私钥字符串
     * @return 私钥
     * @throws Exception 异常
     */
    public static PrivateKey getPrivateKeyByString(String privateKeyString) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(privateKeyString);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 将公钥字符串还原为公钥
     *
     * @param publicKeyString 公钥字符串
     * @return 公钥
     * @throws Exception 异常
     */
    public static PublicKey getPublicKeyByString(String publicKeyString) throws Exception {
        byte[] keyBytes = Base64.decodeBase64(publicKeyString);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(keySpec);
    }

    /**
     * RSA加密
     *
     * @param plainText 明文
     * @param publicKey 公钥
     * @return 密文
     * @throws Exception 异常
     */
    public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        return Base64.encodeBase64String(cipherText);
    }

    /**
     * RSA解密
     *
     * @param cipherText 密文
     * @param privateKey 私钥
     * @return 明文
     * @throws Exception 异常
     */
    public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
        byte[] bytes = Base64.decodeBase64(cipherText);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return new String(cipher.doFinal(bytes), StandardCharsets.UTF_8);
    }

    /**
     * RSA签名
     *
     * @param plainText  明文
     * @param privateKey 私钥
     * @return 签名
     * @throws Exception 异常
     */
    public static String sign(String plainText, PrivateKey privateKey) throws Exception {
        Signature privateSignature = Signature.getInstance("SHA256withRSA");
        privateSignature.initSign(privateKey);
        privateSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = privateSignature.sign();
        return Base64.encodeBase64String(signedBytes);
    }

    /**
     * RSA验签
     *
     * @param plainText 明文
     * @param signature 签名
     * @param publicKey 公钥
     * @return 是否通过验证
     * @throws Exception 异常
     */
    public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
        Signature publicSignature = Signature.getInstance("SHA256withRSA");
        publicSignature.initVerify(publicKey);
        publicSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = Base64.decodeBase64(signature);
        return publicSignature.verify(signedBytes);
    }

    public static void main(String[] args) throws Exception {

        // 生成2048位密钥对
        KeyPair keyPair = generateKeyPair();
        System.out.println("=====公钥=====");
        System.out.println(keyPair.getPublic());
        System.out.println("=====私钥=====");
        System.out.println(keyPair.getPrivate());

        // RSA公钥加密
        System.out.println("=====测试RSA公钥加密=====");
        String testText = "测试文本";
        String encryptText = encrypt(testText, keyPair.getPublic());
        System.out.println(encryptText);

        // RSA私钥解密
        System.out.println("=====测试RSA私钥解密=====");
        String decryptText = decrypt(encryptText, keyPair.getPrivate());
        System.out.println(decryptText);

    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

# 2.2.2 Python封装

# -*- coding: utf-8 -*-

from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes


def create_rsa_keys(code):
    """
    生成RSA私钥和公钥
    :param code: 密码
    :return:
    """
    # 生成 2048 位的 RSA 密钥
    key = RSA.generate(2048)
    encrypted_key = key.exportKey(passphrase=code, pkcs=8, protection="scryptAndAES128-CBC")
    # 生成私钥
    with open('private_rsa_key.bin', 'wb') as f:
        f.write(encrypted_key)
    # 生成公钥
    with open('rsa_public.pem', 'wb') as f:
        f.write(key.publickey().exportKey())


def file_encryption(file_name, public_key):
    """
    文件加密
    :param file_name: 文件路径名
    :param public_key: 公钥
    :return:
    """
    # 二进制只读打开文件,读取文件数据
    with open(file_name, 'rb') as f:
        data = f.read()
    file_name_new = file_name + '.rsa'
    with open(file_name_new, 'wb') as out_file:
        # 收件人秘钥 - 公钥
        recipient_key = RSA.import_key(open(public_key).read())
        # 一个 16 字节的会话密钥
        session_key = get_random_bytes(16)
        # Encrypt the session key with the public RSA key
        cipher_rsa = PKCS1_OAEP.new(recipient_key)
        out_file.write(cipher_rsa.encrypt(session_key))
        # Encrypt the data with the AES session key
        cipher_aes = AES.new(session_key, AES.MODE_EAX)
        cipher_text, tag = cipher_aes.encrypt_and_digest(data)
        out_file.write(cipher_aes.nonce)
        out_file.write(tag)
        out_file.write(cipher_text)
    return file_name_new


def file_decryption(file_name, code, private_key):
    """
    文件解密
    :param file_name: 文件路径名
    :param code: 密码
    :param private_key: 私钥
    :return:
    """
    with open(file_name, 'rb') as f_in:
        # 导入私钥
        private_key = RSA.import_key(open(private_key).read(), passphrase=code)
        # 会话密钥, 随机数, 消息认证码, 机密的数据
        enc_session_key, nonce, tag, cipher_text = [f_in.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1)]
        cipher_rsa = PKCS1_OAEP.new(private_key)
        session_key = cipher_rsa.decrypt(enc_session_key)
        cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
        # 解密
        data = cipher_aes.decrypt_and_verify(cipher_text, tag)
    # 文件重命名
    out_file_name = file_name.replace('.rsa', '')
    with open(out_file_name, 'wb') as f_out:
        f_out.write(data)
    return out_file_name


if __name__ == '__main__':
    create_rsa_keys("test_rsa_key")
    file_encryption("test.txt", "rsa_public.pem")
    file_decryption("test.txt.rsa", "test_rsa_key", "private_rsa_key.bin")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 2.3 AES加密解密

# 2.3.1 Java封装

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Base64;

/**
 * AES加密工具类
 */
public class AesUtility {

    /**
     * 偏移量  AES 为16bytes. DES
     */
    public static final String VIPARA = "0845762876543456";

    /**
     * 编码方式
     */
    public static final String CODE_TYPE = "UTF-8";

    /**
     * 填充类型
     */
    public static final String AES_TYPE = "AES/ECB/PKCS5Padding";

    /**
     * 字符补全
     */
    private static final String[] CONSULT = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B",
            "C", "D", "E", "F", "G"};

    /**
     * AES加密
     *
     * @param cleartext 明文
     * @param aesKey    密钥
     * @return 密文
     */
    public static String encrypt(String cleartext, String aesKey) {
        // 加密方式: AES128(CBC/PKCS5Padding) + Base64
        try {
            if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
                cleartext = completionCodeFor16Bytes(cleartext);
            }
            aesKey = md5(aesKey);
            System.out.println(aesKey);
            // 两个参数,第一个为私钥字节数组, 第二个为加密方式 AES或者DES
            SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
            // 实例化加密类,参数为加密方式,要写全
            // PKCS5Padding比PKCS7Padding效率高,PKCS7Padding可支持IOS加解密
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            // 初始化,此方法可以采用三种方式,按加密算法要求来添加。
            // (1)无第三个参数
            // (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)
            // (3)采用此代码中的IVParameterSpec 加密时使用:ENCRYPT_MODE; 解密时使用:DECRYPT_MODE;
            // CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 加密操作,返回加密后的字节数组,然后需要编码。主要编解码方式有Base64, HEX,
            // UUE,7bit等等。此处看服务器需要什么编码方式
            byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
            Base64.Encoder encoder = Base64.getMimeEncoder();
            return encoder.encodeToString(encryptedData);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 解密
     *
     * @param encrypted
     * @return
     */
    public static String decrypt(String encrypted, String aesKey) {
        try {
            aesKey = md5(aesKey);
            Base64.Decoder decoder = Base64.getMimeDecoder();
            byte[] byteMi = decoder.decode(encrypted);
            SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            // 与加密时不同MODE:Cipher.DECRYPT_MODE
            // CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptedData = cipher.doFinal(byteMi);
            String content = new String(decryptedData, CODE_TYPE);
            if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
                content = resumeCodeOf16Bytes(content);
            }
            return content;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 补全字符
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String completionCodeFor16Bytes(String str) throws UnsupportedEncodingException {
        int num = str.getBytes(CODE_TYPE).length;
        int index = num % 16;
        // 进行加密内容补全操作, 加密内容应该为 16字节的倍数, 当不足16*n字节是进行补全, 差一位时 补全16+1位
        // 补全字符 以 $ 开始,$后一位代表$后补全字符位数,之后全部以0进行补全;
        if (index != 0) {
            StringBuilder stringBuilder = new StringBuilder(str);
            if (16 - index == 1) {
                stringBuilder.append("$").append(CONSULT[16 - 1]).append(addStr(16 - 1 - 1));
            } else {
                stringBuilder.append("$").append(CONSULT[16 - index - 1]).append(addStr(16 - index - 1 - 1));
            }
            str = stringBuilder.toString();
        }
        return str;
    }

    /**
     * 追加字符
     * @param num
     * @return
     */
    public static String addStr(int num) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < num; i++) {
            stringBuilder.append("0");
        }
        return stringBuilder.toString();
    }

    /**
     * 还原字符(进行字符判断)
     * @param str
     * @return
     */
    public static String resumeCodeOf16Bytes(String str) {
        int indexOf = str.lastIndexOf("$");
        if (indexOf == -1) {
            return str;
        }
        String trim = str.substring(indexOf + 1, indexOf + 2).trim();
        int num = 0;
        for (int i = 0; i < CONSULT.length; i++) {
            if (trim.equals(CONSULT[i])) {
                num = i;
            }
        }
        if (num == 0) {
            return str;
        }
        return str.substring(0, indexOf).trim();
    }

    /**
     * md5
     * @param dateString
     * @return
     * @throws Exception
     */
    public static String md5(String dateString) throws Exception {
        byte[] digest = MessageDigest.getInstance("md5").digest(dateString.getBytes(CODE_TYPE));
        StringBuilder md5code = new StringBuilder(new BigInteger(1, digest).toString(16));
        // 如果生成数字未满32位,需要前面补0
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code.insert(0, "0");
        }
        return md5code.toString();
    }

    public static String sampleEncrypt(String clearText, String aesKey) {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
            byte[] b = cipher.doFinal(clearText.getBytes(CODE_TYPE));
            return Base64.getMimeEncoder().encodeToString(b);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public static String sampleDecrypt(String encrypted, String aesKey) {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            Cipher cipher = Cipher.getInstance(AES_TYPE);
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
            byte[] b = cipher.doFinal(Base64.getMimeDecoder().decode(encrypted));
            return new String(b, CODE_TYPE);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        // AES加密算法
        String encrypt = sampleEncrypt("{\"username\":\"admin\",\"password\":\"12345678\"}", "bbbvccc22cabdcbaf399dffff48604fv");
        System.out.println("result:" + encrypt);
        String decrypt = sampleDecrypt("/C3gUT4IKEBdf70O7sjqupwZSRhNKiArQAynSu3+LoCKmiFIb963ZIV2QlbzuQWW", SessionHolder.getWebAesKey());
        System.out.println(decrypt);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

注意:AES的256位密钥加解密可能会报 java.security.InvalidKeyException: Illegal key size or default parameters 异常

报错原因:

在我们安装JRE的目录下有这样一个文件夹:%JAVE_HOME%\jre\lib\security,其中包含有两个.jar文件:“local_policy.jar ”和“US_export_policy.jar”。JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持,所以才会报:“java.security.InvalidKeyException: Illegal key size or default parameters”的异常。
1

解决办法:

方法一:下载单独策略文件,替换原来的文件。

方法二:将jdk升级到JDK1.8.0-161以上版本,就不需要单独策略文件了。

# 2.3.2 Python封装

# -*- coding: utf-8 -*-

import base64
from Crypto.Cipher import AES

'''
采用AES对称加密算法
'''

# str不是16的倍数那就补足为16的倍数
def add_to_16(value):
    while len(value) % 16 != 0:
        value += '\0'
    return str.encode(value)  

# 加密方法
def encrypt_file(key, input_file_path, encoding, output_file_path):
    # 一次性读取文本内容
    with open(input_file_path, 'r', encoding=encoding) as f:
        # print(text) 测试打印读取的数据
        # 待加密文本
        mystr = f.read()
    text = base64.b64encode(mystr.encode('utf-8')).decode('ascii')
    # 初始化加密器
    aes = AES.new(add_to_16(key), AES.MODE_ECB)
    # 先进行aes加密
    encrypt_aes = aes.encrypt(add_to_16(text))
    # 用base64转成字符串形式
    encrypted_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')  # 执行加密并转码返回bytes
    # print(encrypted_text) 测试打印加密数据
    # 写入加密数据到文件
    with open(output_file_path, "w") as bankdata:
        bankdata.write(encrypted_text)

# 解密方法
def decrypt_file(key, file_path, encoding):
    # 密文
    with open(file_path, 'r', encoding=encoding) as f:
        # print(text) 测试打印读取的加密数据
        # 待解密文本
        text = f.read()
    # 初始化加密器
    aes = AES.new(add_to_16(key), AES.MODE_ECB)
    # 优先逆向解密base64成bytes
    base64_decrypted = base64.decodebytes(text.encode(encoding='utf-8'))
    # bytes解密
    decrypted_text = str(aes.decrypt(base64_decrypted),encoding='utf-8')
    decrypted_text = base64.b64decode(decrypted_text.encode('utf-8')).decode('utf-8')
    print(decrypted_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 2.4 数据传输加密方案

# 2.4.1 约定密钥使用AES加密

这种方案要求服务端和客户端约定好加密的AES密钥,在两端分别保留,在处理数据时就用约定好的AES密钥对数据进行加解密。

该方案能应付一些对安全要求不是很高的项目。由于AES密钥是分别放在两端的,客户端并不可靠,别有用心之人是能够获取到AES密钥的,这时就能通过抓包软件篡改传输的数据然后再次加密发送给服务端了,而此时,服务端是不知道数据是被篡改过的。

# 2.4.2 使用AES结合RSA进行加密

RSA加密虽然可靠,但其加密速度较慢,并且不适合对大量数据进行加密,因此单纯的使用RSA加密肯定是不可取的。而AES的加密速度快且对加密数据没有大小限制,但是AES密钥的传输及管理是个问题。可以将二者结合起来使用,用AES加密数据包,用RSA加密随机的AES密钥,这样就可以取长补短了。

以客户端给服务端传输数据为例:

  • 服务端生成一对RSA密钥,其中RSA私钥放在服务端(务必不可泄露),RSA公钥下发给客户端。
  • 客户端使用随机函数生成AES加密要用的AES密钥。
  • 客户端使用随机的AES密钥对要传输的数据进行AES加密。
  • 使用服务端给的RSA公钥对客户端随机生成的AES密钥进行加密。
  • 客户端将使用AES加密的数据以及使用RSA公钥加密的随机AES密钥一起发送给服务端。
  • 服务端拿到数据后,先使用RSA私钥对加密的随机AES密钥进行解密,解密成功即可确定是客户端发来的数据,没有经过他人修改,然后使用解密成功的AES密钥对使用AES加密的数据进行解密,获取最终的数据。

该方案由于服务端的RSA私钥被人是拿不到的,也就没办法解密你要传给服务端的数据,想篡改数据后再提交给服务端就也就做不到了。这种方案实际上已经很安全了,实现了对客户端身份的单向认证以及对传输数据的加密,基本上可以应付绝大部分的项目了。如果还想要加强安全性,可以采用“RSA结合AES实现双向验证”的方案。

# 3. 密码及敏感信息相关

# 3.1 弱密码验证及强密码生成

# 3.1.1 弱密码验证

常见场景:系统超级管理员账号初始化、用户注册账号、用户修改密码等

解决方案:在前后端使用正则验证

校验弱密码的正则表达式如下:

^(?=.*\d)(?=.*[a-zA-Z])(?=.*[~!@#$%^&*])[\da-zA-Z~!@#$%^&*]{8,16}$
1

含义解释:必须8-16位,必须带有特殊字符,必须带有数字,必须带有英文(大写或者小写均可)

# 3.1.2 强密码生成

常见场景:给中间件及数据库设置密码,AES密钥的生成,系统超级管理员账号的初始化等

解决方案:生成强密码

第一种方式:使用系统自带的命令生成(支持MacOS、Linux)

$ openssl rand -hex 32
1

第二种方式:使用Java代码生成

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 生成指定位数的随机密码,密码中需要包含大写字母、小写字母、数字和特殊字符。
 */
public class RandPassword {
    public static final char[] allowedSpecialCharactors = {
            '`', '~', '@', '#', '$', '%', '^', '&',
            '*', '(', ')', '-', '_', '=', '+', '[',
            '{', '}', ']', '\\', '|', ';', ':', '"',
            '\'', ',', '<', '.', '>', '/', '?'};//密码能包含的特殊字符
    private static final int letterRange = 26;
    private static final int numberRange = 10;
    private static final int spCharactorRange = allowedSpecialCharactors.length;
    private static final SecureRandom random = new SecureRandom();
    private int passwordLength;//密码的长度
    private int minVariousType;//密码包含字符的最少种类

    public RandPassword(int passwordLength, int minVariousType) {
        if (minVariousType > CharactorType.values().length) minVariousType = CharactorType.values().length;
        if (minVariousType > passwordLength) minVariousType = passwordLength;
        this.passwordLength = passwordLength;
        this.minVariousType = minVariousType;
    }

    public String generateRandomPassword() {
        char[] password = new char[passwordLength];
        List<Integer> pwCharsIndex = new ArrayList();
        for (int i = 0; i < password.length; i++) {
            pwCharsIndex.add(i);
        }
        List<CharactorType> takeTypes = new ArrayList(Arrays.asList(CharactorType.values()));
        List<CharactorType> fixedTypes = Arrays.asList(CharactorType.values());
        int typeCount = 0;
        while (pwCharsIndex.size() > 0) {
            int pwIndex = pwCharsIndex.remove(random.nextInt(pwCharsIndex.size()));//随机填充一位密码
            Character c;
            if (typeCount < minVariousType) {//生成不同种类字符
                c = generateCharacter(takeTypes.remove(random.nextInt(takeTypes.size())));
                typeCount++;
            } else {//随机生成所有种类密码
                c = generateCharacter(fixedTypes.get(random.nextInt(fixedTypes.size())));
            }
            password[pwIndex] = c.charValue();
        }
        return String.valueOf(password);
    }

    private Character generateCharacter(CharactorType type) {
        Character c = null;
        int rand;
        switch (type) {
            case LOWERCASE://随机小写字母
                rand = random.nextInt(letterRange);
                rand += 97;
                c = new Character((char) rand);
                break;
            case UPPERCASE://随机大写字母
                rand = random.nextInt(letterRange);
                rand += 65;
                c = new Character((char) rand);
                break;
            case NUMBER://随机数字
                rand = random.nextInt(numberRange);
                rand += 48;
                c = new Character((char) rand);
                break;
            case SPECIAL_CHARACTOR://随机特殊字符
                rand = random.nextInt(spCharactorRange);
                c = new Character(allowedSpecialCharactors[rand]);
                break;
        }
        return c;
    }

    public static void main(String[] args) {
        System.out.println(new RandPassword(32, 4).generateRandomPassword());
    }
}

enum CharactorType {
    LOWERCASE,
    UPPERCASE,
    NUMBER,
    SPECIAL_CHARACTOR
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

# 3.2 禁止密码明文传输

常见场景:用户注册、用户登录、修改密码等

解决方案:使用加密算法进行加密

在进行包含密码的操作时,前后端必须对数据进行加密,与加密算法有关的见本文的第1节。通常在业务系统里我们选用AES对称加密算法,前端将请求包整个加密成一个字符串,以body的形式传输,后端进行解密及验证。入库的时候还要对密码单独进行加密存储。具体实现见本文的第3.1节。

# 3.3 中间件服务设置密码

对于ElasticSearch、Redis这类默认不要求设置密码的中间件,经常会有人在部署的时候不设置密码,这样非常不安全,务必要设置上。

# 3.3.1 ElasticSearch设置密码

部署ElasticSearch

$ docker pull elasticsearch:7.16.2
$ docker run -d --name es \
-p 9200:9200 -p 9300:9300 \
-v /root/docker/es/data:/usr/share/elasticsearch/data \
-v /root/docker/es/config:/usr/share/elasticsearch/config \
-v /root/docker/es/plugins:/usr/share/elasticsearch/plugins \
-e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms1g -Xmx1g" \
elasticsearch:7.16.2
$ docker update es --restart=always
1
2
3
4
5
6
7
8
9

注意事项:

1)Elasticsearch请选择7.16.0之后的版本,之前的所有版本都使用了易受攻击的 Log4j2版本,存在严重安全漏洞。

2)ES_JAVA_OPTS="-Xms1g -Xmx1g"只是一个示例,内存设置的少了会导致数据查询速度变慢,具体设置多少要根据业务需求来定,一般而言公司的实际项目要设置8g内存以上。

开启xpack并设置密码

进入容器进行配置

$ docker exec -it es /bin/bash 
$ cd config
$ chmod o+w elasticsearch.yml
$ vi elasticsearch.yml
1
2
3
4

其中,在 elasticsearch.yml 文件的末尾添加如下配置

xpack.security.enabled: true    
1

然后把权限修改回来,重启容器,设置账号密码,浏览器访问http://IP:9200地址即可(用 elastic账号 和自己设置的密码登录即可)

$ chmod o-w elasticsearch.yml
$ exit
$ docker restart es
$ docker exec -it es /bin/bash 
$ ./bin/elasticsearch-setup-passwords interactive   // 然后设置账号密码
1
2
3
4
5

# 3.3.2 Redis设置密码

$ docker pull redis:3.2.8
$ docker run --name redis -p 6379:6379 -d redis:3.2.8 --requirepass "your_password"
$ docker update redis --restart=always
1
2
3

# 3.4 敏感信息脱敏

常见场景:用户注册、用户信息修改等

解决方案:敏感信息数据脱敏

针对身份证号、姓名、手机号、邮箱、车牌号等等敏感信息,入库之前要对数据进行脱敏,以防一旦敏感泄露数据造成不可挽回的后果。敏感信息脱敏可以使用如下工具类来实现:

import org.apache.commons.lang3.StringUtils;

/**
 * 字段脱敏工具类
 */
public class SensitiveFieldUtils {

    /**
     * @param userName 名字
     * @return 脱敏结果
     */
    public static String chineseName(String userName) {
        if (StringUtils.isEmpty(userName)) {
            return "";
        }
        String name = StringUtils.left(userName, 1);
        return StringUtils.rightPad(name, StringUtils.length(userName), "*");
    }

    /**
     * @param idCard 身份证号
     * @return 脱敏结果
     */
    public static String idCard(String idCard) {
        if (StringUtils.isEmpty(idCard)) {
            return "";
        }
        String id = StringUtils.right(idCard, 4);
        return StringUtils.leftPad(id, StringUtils.length(idCard), "*");
    }

    /**
     * @param phone 手机号
     * @return 脱敏结果
     */
    public static String telephone(String phone) {
        if (StringUtils.isEmpty(phone)) {
            return "";
        }
        return StringUtils.left(phone, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(phone, 4), StringUtils.length(phone), "*"), "***"));
    }

    /**
     * @param address 地址信息
     * @param sensitiveSize 敏感信息长度
     * @return 脱敏结果
     */
    public static String address(String address, int sensitiveSize) {
        if (StringUtils.isBlank(address)) {
            return "";
        }
        int length = StringUtils.length(address);
        return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
    }

    /**
     * @param email 邮箱
     * @return 脱敏结果
     */
    public static String email(String email) {
        if (StringUtils.isBlank(email)) {
            return "";
        }
        int index = StringUtils.indexOf(email, "@");
        if (index <= 1) {
            return email;
        } else {
            return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
        }
    }


    /**
     * @param cardNum 银行卡号
     * @return 脱敏结果
     */
    public static String bankCard(String cardNum) {
        if (StringUtils.isBlank(cardNum)) {
            return "";
        }
        return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******"));
    }

    public static void main(String[] args) {

        // 测试姓名脱敏(张**)
        String name = chineseName("张三丰");
        System.out.println("name = " + name);

        // 测试身份证号脱敏(**************7812)
        String idCard = idCard("123456781234567812");
        System.out.println("idCard = " + idCard);

        // 测试手机号脱敏(186****0000)
        String telephone = telephone("18600000000");
        System.out.println("telephone = " + telephone);

        // 测试地址脱敏(天津市滨海新区*********)
        String address = address("天津市滨海新区经济开发区第三大街", 9);
        System.out.println("address = " + address);

        // 测试邮箱脱敏(t***@163.com)
        String email = email("[email protected]");
        System.out.println("email = " + email);

        // 测试银行卡号脱敏(622316******6887)
        String bankCard = bankCard("6223165905596887");
        System.out.println("bankCard = " + bankCard);
        
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

# 4. Spring Security相关问题

# 4.1 通过body接收数据

需求描述:项目过安全检测的时候,不允许在参数中探测到username、password等敏感信息,即便它是密文的,因此打算将整个入参都加密成一个无规则字符串,通过body直接传输。

问题描述:在使用 Spring Security 的时候,发现用 body 传参的话,后台不能获取到数据,查看UsernamePasswordAuthenticationFilter 源码发现,是直接从 Request 获取的,而不是从 RequestBody 中获取的。

    //获取密码
    protected String obtainPassword(HttpServletRequest request) {
         return request.getParameter(passwordParameter);
    }
    //获取用户名
    protected String obtainUsername(HttpServletRequest request) {
         return request.getParameter(usernameParameter);
    }
1
2
3
4
5
6
7
8

解决办法:重写 UsernamePasswordAuthenticationFilter 类,配置自定义过滤器

新建 UserAuthenticationFilter.java

import com.alibaba.fastjson.JSONObject;
import com.yoyo.admin.framework.util.AesUtility;
import org.apache.commons.io.IOUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义过滤器,重写 UsernamePasswordAuthenticationFilter,从body获取参数
 */
public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();

    @Override
    protected String obtainPassword(HttpServletRequest request) {
        String password = this.getBodyParams(request).get(SPRING_SECURITY_FORM_PASSWORD_KEY);
        if(!StringUtils.isEmpty(password)){
            return password;
        }
        return super.obtainPassword(request);
    }

    @Override
    protected String obtainUsername(HttpServletRequest request) {
        String username = this.getBodyParams(request).get(SPRING_SECURITY_FORM_USERNAME_KEY);
        if(!StringUtils.isEmpty(username)){
            return username;
        }
        return super.obtainUsername(request);
    }

    /**
     * 获取body参数  body中的参数只能获取一次
     * @param request
     * @return
     */
    private Map<String,String> getBodyParams(HttpServletRequest request){
        Map<String,String> bodyParams =  threadLocal.get();
        if(bodyParams == null){
            bodyParams = new HashMap<>();
        }
        try (InputStream is = request.getInputStream()) {
            String bodyString = IOUtils.toString(is, String.valueOf(StandardCharsets.UTF_8));
 		        // 对接收到的 bodyString进行解密(此代码略)
            Map<String, Object> tempMap = JSONObject.parseObject(bodyString);
            bodyParams.put("password",tempMap.get("password").toString());
            bodyParams.put("username",tempMap.get("username").toString());
        } catch (IOException e) {
        }
        threadLocal.set(bodyParams);

        return bodyParams;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

修改 WebSecurityConfig.java

改动一:在 protected void configure(HttpSecurity httpSecurity) throws Exception 里配置自定义过滤器。

        // 配置自定义过滤器,增加 post json 支持
        httpSecurity.addFilterAt(UserAuthenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);
1
2

改动二:添加如下方法。

    private UserAuthenticationFilter UserAuthenticationFilterBean() throws Exception {
        UserAuthenticationFilter userAuthenticationFilter = new UserAuthenticationFilter();
        userAuthenticationFilter.setAuthenticationManager(super.authenticationManager());
        userAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        userAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        return userAuthenticationFilter;
    }
1
2
3
4
5
6
7

注:authenticationSuccessHandler、authenticationFailureHandler分别是登录成功、失败的处理类,此处略。

# 4.2 多次登录失败后账户锁定

[1] 实现多次登录失败锁定的原理

一般来说实现这个需求,我们需要针对每一个用户记录登录失败的次数nLock和锁定账户的到期时间releaseTime。具体你是把这2个信息存储在mysql、还是文件中、还是redis中等等,完全取决于你对你所处的应用架构适用性的判断。具体的实现逻辑无非就是:

  • 登陆失败之后,从存储中将nLock取出来加1。
  • 如果nLock大于登陆失败阈值(比如3次),则将nLock=0,然后设置releaseTime为当前时间加上锁定周期。通过setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 如果nLock小于等于1,则将nLock再次存起来。
  • 在一个合适的时机,将锁定状态重置为setAccountNonLocked(true)。

这是一种非常典型的实现方式,可以使用开源的ratelimitj来实现。它的功能主要是为API访问进行限流,也就是说可以通过制定规则限制API接口的访问频率。那恰好登录验证接口也是API的一种啊,我们正好也需要限制它在一定的时间内的访问次数。

[2] 多次登录失败锁定的具体实现

首先需要将ratelimitj通过maven引入到项目里来。这里使用的是内存存储的版本,还有redis存储的版本,可以根据自己的实际情况选用。

        <dependency>
            <groupId>es.moki.ratelimitj</groupId>
            <artifactId>ratelimitj-inmemory</artifactId>
            <version>0.4.1</version>
        </dependency>
1
2
3
4
5

CheckLockUtility.java

import es.moki.ratelimitj.core.limiter.request.RequestLimitRule;
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter;
import es.moki.ratelimitj.inmemory.request.InMemorySlidingWindowRequestRateLimiter;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 多次登录失败账号锁定工具类
 */
public class AccountLockUtility {

    // 规则定义:10分钟之内3次错误机会,达到就触发账号锁定
    private static Set<RequestLimitRule> rules = Collections.singleton(RequestLimitRule.of(10, TimeUnit.MINUTES,3));
    private static RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules);

    // 计数器加1,并判断该用户是否已经到了触发了锁定规则
    public static boolean checkLock(String userId) {
        return limiter.overLimitWhenIncremented(userId);
    }

    // 重置锁定
    public static void resetLock(String userId){
        limiter.resetLimit(userId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

在AuthenticationSuccessHandler

        //从request.getSession中获取登录用户
        String userId = request.getSession().getId();
        boolean reachLimit = AccountLockUtility.checkLock(userId);
        if(reachLimit) {
            try {
                ... // 返回“多次登录失败,账户已被锁定,请10分钟后重试”的错误消息
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else{
            AccountLockUtility.resetLock(userId); // 重置锁定
            ... // 编写登录成功的业务逻辑
        }
1
2
3
4
5
6
7
8
9
10
11
12
13

AuthenticationFailureHandler里分别编写账户锁定校验。

        //从request.getSession中获取登录用户
        String userId = request.getSession().getId();
        boolean reachLimit = AccountLockUtility.checkLock(userId);
        if(reachLimit) {
            try {
                ... // 返回“多次登录失败,账户已被锁定,请10分钟后重试”的错误消息
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else{
            ... // 编写登录失败的业务逻辑
        }
1
2
3
4
5
6
7
8
9
10
11
12

# 4.3 登录登出接口添加Swagger

情景描述:有些场景我们需要手动将接口添加到Swagger中,比如非SpringMVC注解暴露接口(如定义在filter中),无法通过这种注解方式生成api接口文档。SpringSecurity的用户名密码登录接口,就是在filter中进行了拦截,因此在Swagger中看不到该登录接口,这样在平时的开发测试中,非常不方便。

解决思路:通过实现swagger提供的插件ApiListingScannerPlugin,可以手动将接口添加到swagger文档里。

代码实现:SpringSecurityApis.java

import com.fasterxml.classmate.TypeResolver;
import org.apache.commons.compress.utils.Sets;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ApiListingScannerPlugin;
import springfox.documentation.spi.service.contexts.DocumentationContext;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;


/**
 * 由于 Spring Security 的登录、登出接口是通过Filter实现,导致 Swagger 无法获取其信息。
 * 这里手动将登录、登出接口注册到Swagger中,在Swagger-UI才能展示,方便调用。
 */
@Component
public class SpringSecurityApis implements ApiListingScannerPlugin {

    /**
     * Implement this method to manually add ApiDescriptions
     * 实现此方法可手动添加ApiDescriptions
     *
     * @param context - Documentation context that can be used infer documentation context
     * @return List of {@link ApiDescription}
     * @see ApiDescription
     */
    @Override
    public List<ApiDescription> apply(DocumentationContext context) {

        // 额外添加登录接口的文档
        Operation loginOperation = new OperationBuilder(new CachingOperationNameGenerator())
                .method(HttpMethod.POST)
                .summary("系统登录")
                // 接收参数格式
                .consumes(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
                // 返回参数格式
                .produces(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
                .tags(Sets.newHashSet("系统管理"))
                .uniqueId("login")
                .parameters(Collections.singletonList(
                        new ParameterBuilder()
                                .description("用户名与密码使用AES加密后得到的字符串")
                                .type(new TypeResolver().resolve(String.class))
                                .name("aes_str")
                                .parameterType("body")
                                .parameterAccess("access")
                                .required(true)
                                .modelRef(new ModelRef("text"))
                                .build()
                ))
                .responseMessages(Collections.singleton(
                        new ResponseMessageBuilder().code(200).message("请求成功")
                                .responseModel(new ModelRef(
                                        "xyz.gits.boot.common.core.response.RestResponse")
                                ).build()))
                .build();

        ApiDescription loginApiDescription = new ApiDescription("auth", "/auth/login", "登录接口",
                Collections.singletonList(loginOperation), false);

        // 额外添加退出登录接口的文档
        Operation logoutOperation = new OperationBuilder(new CachingOperationNameGenerator())
                .method(HttpMethod.POST)
                .summary("系统退出登录")
                // 接收参数格式
                .consumes(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
                // 返回参数格式
                .produces(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
                .parameters(Collections.emptyList())
                .uniqueId("logout")
                .tags(Sets.newHashSet("系统管理"))
                .responseMessages(Collections.singleton(
                        new ResponseMessageBuilder().code(200).message("请求成功")
                                .responseModel(new ModelRef(
                                        "xyz.gits.boot.common.core.response.RestResponse")
                                ).build()))
                .build();

        ApiDescription logoutApiDescription = new ApiDescription("auth", "/auth/logout", "退出登录接口",
                Collections.singletonList(logoutOperation), false);

        return Arrays.asList(loginApiDescription, logoutApiDescription);

    }

    /**
     * 是否使用此插件
     *
     * @param documentationType swagger文档类型
     * @return true 启用
     */
    @Override
    public boolean supports(DocumentationType documentationType) {
        return DocumentationType.SWAGGER_2.equals(documentationType);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

# 5. 业务系统增加日志审计与短信验证码

# 5.1 增加日志审计模块

在业务系统中增加日志审计,可以通过使用Spring AOP和自定义注解来实现。需要特别注意一些敏感操作的日志记录,以下是一些常见的敏感操作:

  • 登录和退出:包括成功和失败的登录尝试,以及用户的退出操作。
  • 密码修改:任何用户的密码修改行为都应该被记录下来。
  • 用户角色和权限的更改:任何用户角色和权限的更改,包括添加、删除和修改角色或权限。
  • 敏感数据的访问:如用户个人信息,财务信息,业务关键数据等。
  • 数据的创建、修改和删除:这包括数据库记录的更改,文件或目录的创建,修改和删除等。
  • 系统设置的更改:包括修改系统参数,系统启动和关闭等。

# 5.1.1 项目结构

在项目里添加 logger 模块,然后在具体的接口加上注解,即可实现操作日志审计。

logger
├── annotation
│   ├── CreateLog.java
│   ├── DeleteLog.java
│   └── UpdateLog.java
├── domain
│   ├── LogContentObject.java
│   ├── OperationLog.java
│   └── OperationLogRepository.java
├── enumeration
│   ├── OperationType.java
│   └── OperationTypeConverter.java
└── service
    ├── OperationLogService.java
    └── TerminalService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注:这里仅仅是封装代码部分,还要结合具体业务实体类和接口注解才能实现此功能,import com.yoyo.admin.xxx的代码此处略去。

# 5.1.2 代码实现

./annotation/CreateLog.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreateLog {

    String target();

    String operation() default "新增";
}
1
2
3
4
5
6
7
8
9
10
11
12
13

./annotation/DeleteLog.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DeleteLog {
    String target();

    String operation() default "删除";
}
1
2
3
4
5
6
7
8
9
10
11
12

./annotation/UpdateLog.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UpdateLog {
    String target();

    String operation() default "更新";
}
1
2
3
4
5
6
7
8
9
10
11
12

./domain/LogContentObject.java

import lombok.Data;

import java.io.Serializable;

@Data
public class LogContentObject implements Serializable {

    private String name;

    private Object data;

}
1
2
3
4
5
6
7
8
9
10
11
12

./domain/OperationLog.java

import com.yoyo.admin.common.logger.enumeration.OperationType;
import com.yoyo.admin.common.logger.enumeration.OperationTypeConverter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.util.Date;

/**
 * 操作日志
 */
@Data
@Entity
@ApiModel(value = "OperationLog", description = "操作日志")
@Table(name = "operation_log")
@org.hibernate.annotations.Table(appliesTo = "operation_log", comment = "操作日志表")
public class OperationLog {

    /**
     * 操作日志id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @ApiModelProperty(value = "操作日志id")
    @Column(name = "id", columnDefinition = "bigint COMMENT '操作日志id'")
    private Long id;

    /**
     * 操作类型id
     */
    @ApiModelProperty(value = "操作类型id")
    @Column(name = "operation_type_id", columnDefinition = "bigint COMMENT '操作类型id'")
    @Convert(converter = OperationTypeConverter.class)
    private OperationType operationType;

    /**
     * 终端名称
     */
    @ApiModelProperty(value = "终端名称")
    @Column(name = "terminal_name", length = 20, columnDefinition = "varchar(255) COMMENT '终端名称'")
    private String terminalName;

    /**
     * web用户Id
     */
    @ApiModelProperty(value = "web用户Id")
    @Column(name = "web_user_id", columnDefinition = "bigint COMMENT 'web用户Id'")
    private Long webUserId;

    /**
     * 小程序/公众号用户Id
     */
    @ApiModelProperty(value = "小程序/公众号用户Id")
    @Column(name = "wechat_user_id", length = 50, columnDefinition = "varchar(50) COMMENT '小程序/公众号用户Id'")
    private String wechatUserId;

    /**
     * 用户名
     */
    @ApiModelProperty(value = "用户名")
    @Column(name = "user_name", length = 50, columnDefinition = "varchar(50) COMMENT '用户名'")
    private String userName;

    /**
     * ip地址
     */
    @ApiModelProperty(value = "ip地址")
    @Column(name = "ip_address", length = 50, columnDefinition = "varchar(50) COMMENT 'ip地址'")
    private String ipAddress;

    /**
     * 具体操作,由注解传来
     */
    @ApiModelProperty(value = "具体操作,由注解传来")
    @Column(name = "operation", length = 50, columnDefinition = "varchar(50) COMMENT '具体操作,由注解传来'")
    private String operation;

    /**
     * 操作内容
     */
    @ApiModelProperty(value = "操作内容")
    @Column(name = "content", columnDefinition = "text COMMENT '操作内容'")
    private String content;

    /**
     * 操作对象
     */
    @ApiModelProperty(value = "操作对象")
    @Column(name = "target", length = 50, columnDefinition = "varchar(50) COMMENT '操作对象'")
    private String target;

    /**
     * 备注
     */
    @ApiModelProperty(value = "备注")
    @Column(name = "remark", length = 1024, columnDefinition = "varchar(1024) COMMENT '备注'")
    private String remark;

    /**
     * 创建时间
     */
    @CreationTimestamp
    @ApiModelProperty(value = "创建时间")
    @Column(name = "create_time", updatable = false, columnDefinition = "datetime COMMENT '创建时间'")
    private Date createTime;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

./domain/OperationLogRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * 操作日志
 */
public interface OperationLogRepository extends JpaRepository<OperationLog, Long>, JpaSpecificationExecutor<OperationLog> {

    /**
     * 根据id获取操作日志
     * @param id
     * @return
     */
    OperationLog findFirstById(Long id);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

./enumeration/OperationType.java

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 操作类型
 */
public enum OperationType {
    Create(1, "新增"), Update(2, "更新"), Delete(3, "删除"),
    Login(4, "登录"), Logout(5, "登出");

    private final Integer id;
    private final String name;

    OperationType(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    @JsonValue
    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    /**
     * 按id获取
     * @param id
     * @return
     */
    @JsonCreator
    public static OperationType getById(Integer id) {
        if (id == null) {
            return null;
        }
        for (OperationType item : OperationType.values()) {
            if (item.getId().equals(id)) {
                return item;
            }
        }
        return null;
    }

    /**
     * 获取列表
     * @return
     */
    public static List<Map<String, Object>> listAll() {
        List<Map<String, Object>> list = new ArrayList<>();
        for (OperationType item : OperationType.values()) {
            Map<String, Object> map = new HashMap<>();
            map.put("id", item.getId());
            map.put("name", item.getName());
            list.add(map);
        }
        return list;
    }

    /**
     * 解析从body中提取的操作类型id参数为操作类型
     *
     * @param idObject    id对象
     * @param isNecessary 是否是必填,一般新建时为true,更新时为false
     * @return 操作类型
     * @throws RuntimeException 用于返回给前端的错误提示
     */
    public static OperationType parseBodyPlatformParam(Object idObject, boolean isNecessary) throws RuntimeException {
        if (idObject == null) {
            if (isNecessary) {
                throw new RuntimeException("未指定操作类型id");
            } else {
                return null;
            }
        }
        int id;
        try {
            id = Integer.parseInt(idObject.toString());
        } catch (Exception ex) {
            throw new RuntimeException("指定的操作类型id格式不正确");
        }
        OperationType operationType = getById(id);
        if (operationType == null) {
            throw new RuntimeException("指定的操作类型id不正确");
        }
        return operationType;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

./enumeration/OperationTypeConverter.java

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

/**
 * 操作类型转换
 */
@Converter
public class OperationTypeConverter implements AttributeConverter<OperationType, Integer> {

    @Override
    public Integer convertToDatabaseColumn(OperationType enumItem) {
        // 这里要加上判空,不然实体类的相应字段为空时会报错
        if (enumItem == null) {
            return null;
        }
        return enumItem.getId();
    }

    @Override
    public OperationType convertToEntityAttribute(Integer id) {
        if (id == null) {
            return null;
        }
        return OperationType.getById(id);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

./service/OperationLogService.java

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yoyo.admin.common.domain.Dept;
import com.yoyo.admin.common.domain.Role;
import com.yoyo.admin.common.domain.User;
import com.yoyo.admin.common.logger.annotation.CreateLog;
import com.yoyo.admin.common.logger.annotation.DeleteLog;
import com.yoyo.admin.common.logger.annotation.UpdateLog;
import com.yoyo.admin.common.logger.domain.LogContentObject;
import com.yoyo.admin.common.logger.domain.OperationLog;
import com.yoyo.admin.common.logger.domain.OperationLogRepository;
import com.yoyo.admin.common.logger.enumeration.OperationType;
import com.yoyo.admin.common.utils.PageData;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 操作日志
 */
@Service
@Slf4j
public class OperationLogService {

    private OperationLogRepository operationLogRepository;
    private TerminalService terminalService;
    private final ObjectMapper _objectMapper = new ObjectMapper();
    private final SimpleDateFormat _simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Autowired
    public void setOperationLogRepository(OperationLogRepository operationLogRepository) {
        this.operationLogRepository = operationLogRepository;
    }

    @Autowired
    public void setTerminalService(TerminalService terminalService) {
        this.terminalService = terminalService;
    }

    /**
     * 按id获取操作日志
     * @param id
     * @return
     */
    public OperationLog get(Long id) {
        return operationLogRepository.findFirstById(id);
    }

    /**
     * 操作日志查询
     * @param operationType
     * @param terminalNames
     * @param userName
     * @param ipAddress
     * @param target
     * @param operation
     * @param beginTime
     * @param endTime
     * @param remark
     * @param page
     * @param pageSize
     * @return
     */
    public PageData<OperationLog> list(OperationType operationType, List<String> terminalNames, String userName,
                                       String ipAddress, String target, String operation, Date beginTime, Date endTime,
                                       String remark, int page, int pageSize) {
        Specification<OperationLog> specification = (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicateList = new ArrayList<>();
            if (operationType != null) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("operationType")));
                predicateList.add(criteriaBuilder.equal(root.get("operationType"), operationType));
            }
            if (terminalNames != null && terminalNames.size() > 0) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("terminalName")));
                CriteriaBuilder.In<String> in = criteriaBuilder.in(root.get("terminalName"));
                terminalNames.forEach(in::value);
                predicateList.add(in);
            }
            if (userName != null && !userName.isEmpty()) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("userName")));
                predicateList.add(criteriaBuilder.like(root.get("userName"), "%" + userName + "%"));
            }
            if (ipAddress != null && !ipAddress.isEmpty()) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("ipAddress")));
                predicateList.add(criteriaBuilder.like(root.get("ipAddress"), "%" + ipAddress + "%"));
            }
            if (target != null && !target.isEmpty()) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("target")));
                predicateList.add(criteriaBuilder.equal(root.get("target"), target));
            }
            if (operation != null && !operation.isEmpty()) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("operation")));
                predicateList.add(criteriaBuilder.like(root.get("operation"), "%" + operation + "%"));
            }
            if (beginTime != null) {
                predicateList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), beginTime));
            }
            if (endTime != null) {
                predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("createTime"), endTime));
            }
            if (remark != null && !remark.isEmpty()) {
                predicateList.add(criteriaBuilder.isNotNull(root.get("remark")));
                predicateList.add(criteriaBuilder.like(root.get("remark"), "%" + remark + "%"));
            }
            Predicate[] predicates = new Predicate[predicateList.size()];
            return criteriaBuilder.and(predicateList.toArray(predicates));
        };
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        Page<OperationLog> specialities = operationLogRepository.findAll(specification, PageRequest.of(page - 1, pageSize, sort));
        return new PageData<>(specialities, page, pageSize);
    }

    /**
     * 新增操作日志
     * @param joinPoint
     * @param webUserId
     * @param wechatUserId
     * @param userName
     * @param ipAddress
     */
    public void create(JoinPoint joinPoint, Long webUserId, String wechatUserId, String userName, String ipAddress) {
        Method method = getMethodByName(joinPoint.getTarget(), joinPoint.getSignature().getName());
        if (method == null) {
            return;
        }
        Annotation annotation = getOperationLogAnnotationFromMethod(method);
        OperationType operationType = null;
        String target = null;
        String operation = null;
        StringBuilder remark = new StringBuilder();
        if (annotation != null) {
            operationType = getOperationTypeByAnnotation(annotation);
            target = getTargetFromAnnotation(annotation);
            operation = getOperationFromAnnotation(annotation);
        }
        OperationLog operationLog = new OperationLog();
        operationLog.setOperationType(operationType);
        operationLog.setTerminalName(terminalService.getTerminalName());
        operationLog.setWebUserId(webUserId);
        operationLog.setWechatUserId(wechatUserId);
        operationLog.setUserName(userName);
        operationLog.setIpAddress(ipAddress);
        operationLog.setOperation(operation);
        operationLog.setContent(packageParamToJson(method, joinPoint.getArgs(), remark));
        operationLog.setTarget(target);
        operationLog.setRemark(remark.toString());
        operationLogRepository.save(operationLog);
    }

    /**
     * @param object     对象
     * @param methodName 需要的方法的方法名
     * @return 方法
     */
    public Method getMethodByName(Object object, String methodName) {
        Class<?> objectClass = object.getClass();
        Method[] methods = objectClass.getDeclaredMethods();
        for (int i = 0; i <= methods.length - 1; i++) {
            Method method = methods[i];
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return null;
    }

    /**
     * 根据方法获取操作日志注解
     * @param method
     * @return
     */
    private Annotation getOperationLogAnnotationFromMethod(Method method) {
        Annotation[] annotations = method.getDeclaredAnnotations();
        for (int i = 0; i <= annotations.length - 1; i++) {
            Annotation annotation = annotations[i];
            Class<?> clazz = annotation.annotationType();
            if (clazz.equals(CreateLog.class) || clazz.equals(UpdateLog.class) || clazz.equals(DeleteLog.class)) {
                return annotation;
            }
        }
        return null;
    }

    /**
     * 根据操作日志注解判断操作类型
     * @param annotation
     * @return
     */
    private OperationType getOperationTypeByAnnotation(Annotation annotation) {
        if (annotation.annotationType().equals(CreateLog.class)) {
            return OperationType.Create;
        }
        if (annotation.annotationType().equals(UpdateLog.class)) {
            return OperationType.Update;
        }
        if (annotation.annotationType().equals(DeleteLog.class)) {
            return OperationType.Delete;
        }
        return null;
    }

    /**
     * 从注解中获取target
     * @param annotation
     * @return
     */
    private String getTargetFromAnnotation(Annotation annotation) {
        if (annotation.annotationType().equals(CreateLog.class)) {
            CreateLog createLog = (CreateLog) annotation;
            return createLog.target();
        }
        if (annotation.annotationType().equals(UpdateLog.class)) {
            UpdateLog updateLog = (UpdateLog) annotation;
            return updateLog.target();
        }
        if (annotation.annotationType().equals(DeleteLog.class)) {
            DeleteLog deleteLog = (DeleteLog) annotation;
            return deleteLog.target();
        }
        return null;
    }

    /**
     * 从注解中获取operation
     * @param annotation
     * @return
     */
    private String getOperationFromAnnotation(Annotation annotation) {
        if (annotation.annotationType().equals(CreateLog.class)) {
            CreateLog createLog = (CreateLog) annotation;
            return createLog.operation();
        }
        if (annotation.annotationType().equals(UpdateLog.class)) {
            UpdateLog updateLog = (UpdateLog) annotation;
            return updateLog.operation();
        }
        if (annotation.annotationType().equals(DeleteLog.class)) {
            DeleteLog deleteLog = (DeleteLog) annotation;
            return deleteLog.operation();
        }
        return null;
    }

    /**
     * 将指定方法的参数打包成json
     * @param method
     * @param args
     * @param remark
     * @return
     */
    private String packageParamToJson(Method method, Object[] args, StringBuilder remark) {
        Map<String, Object> map = new HashMap<>();
        Parameter[] parameters = method.getParameters();
        if (parameters.length == args.length) {
            for (int i = 0; i <= parameters.length - 1; i++) {
                LogContentObject contentObject = simplifyParameter(parameters[i].getName(), args[i], 0, remark);
                if (contentObject != null && contentObject.getName() != null) {
                    map.put(contentObject.getName(), contentObject.getData());
                }
            }
        }
        try {
            return _objectMapper.writeValueAsString(map);
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    private LogContentObject simplifyParameter(String paramName, Object data, Integer deep, StringBuilder remark) {
        if (data == null || deep > 3) {
            return null;
        }
        LogContentObject contentObject = new LogContentObject();
        contentObject.setName(paramName);
        //如果是基本类型,直接返回
        if (isBaseType(data)) {
            contentObject.setData(data);
            return contentObject;
        }
        //如果是枚举类型,返回枚举名
        if (data.getClass().isEnum()) {
            contentObject.setData(((Enum<?>) data).name());
            return contentObject;
        }
        //如果是列表,处理其中的每一个元素
        if (data instanceof List) {
            List<Object> list = new ArrayList<>();
            List<?> dataList = (List<?>) data;
            String name = paramName;
            for (int i = 0; i <= dataList.size() - 1; i++) {
                LogContentObject listLogContentObject = simplifyParameter(null, dataList.get(i), deep + 1, remark);
                if (listLogContentObject == null) {
                    continue;
                }
                if (listLogContentObject.getName() != null) {
                    name = listLogContentObject.getName();
                }
                list.add(listLogContentObject.getData());
            }
            contentObject.setName(name);
            contentObject.setData(list);
            return contentObject;
        }
        //如果是文件,直接返回字符串"文件"
        if (data instanceof InputStream || data instanceof File) {
            contentObject.setData("文件");
            return contentObject;
        }
        if (data instanceof Role) {
            contentObject.setName("角色");
            contentObject.setData(simplifyRole((Role) data, remark));
            return contentObject;
        }
        if (data instanceof User) {
            contentObject.setName("用户");
            contentObject.setData(simplifyUser((User) data, remark));
            return contentObject;
        }
        if (data instanceof Dept) {
            contentObject.setName("部门");
            contentObject.setData(simplifyDept((Dept) data));
            return contentObject;
        }
        //如果是其他类型,返回数据为一个Map,内部保存字段名和字段值
        Map<String, Object> map = new HashMap<>();
        Field[] fields = data.getClass().getDeclaredFields();
        for (int i = 0; i <= fields.length - 1; i++) {
            LogContentObject filedLogContentObject = null;
            Field field = fields[i];
            field.setAccessible(true);
            try {
                filedLogContentObject = simplifyParameter(field.getName(), field.get(data), deep + 1, remark);
            } catch (IllegalAccessException ex) {
                log.error(ex.getMessage());
            }
            if (filedLogContentObject != null) {
                map.put(filedLogContentObject.getName(), filedLogContentObject.getData());
            }
        }
        contentObject.setData(map);
        return contentObject;
    }

    private Map<String, Object> simplifyRole(Role role, StringBuilder remark) {
        Map<String, Object> map = new HashMap<>();
        if (role != null) {
            map.put("id", role.getId());
            map.put("部门类型编号", role.getCode());
            map.put("部门类型名称", role.getName());
            remark.append(role.getName());
        }
        return map;
    }

    private Map<String, Object> simplifyUser(User user, StringBuilder remark) {
        Map<String, Object> map = new HashMap<>();
        if (user != null) {
            map.put("用户id", user.getId());
            map.put("用户姓名", user.getName());
            map.put("部门id", user.getDept() != null ? user.getDept().getId() : null);
            remark.append(user.getName()).append("(").append(user.getUsername()).append(")").append(" ");
        }
        return map;
    }

    private Map<String, Object> simplifyDept(Dept dept) {
        Map<String, Object> map = new HashMap<>();
        if (dept != null) {
            map.put("id", dept.getId());
            map.put("名称", dept.getName());
            map.put("上级部门", dept.getParentDept() != null ? dept.getParentDept().getName() : null);
        }
        return map;
    }

    private static boolean isBaseType(Object object) {
        if (object instanceof Boolean) {
            return true;
        }
        if (object instanceof Character) {
            return true;
        }
        if (object instanceof Byte) {
            return true;
        }
        if (object instanceof Short) {
            return true;
        }
        if (object instanceof Integer) {
            return true;
        }
        if (object instanceof Long) {
            return true;
        }
        if (object instanceof Float) {
            return true;
        }
        if (object instanceof Double) {
            return true;
        }
        if (object instanceof Date) {
            return true;
        }
        return object instanceof String;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

./service/TerminalService.java

import org.springframework.stereotype.Component;

/**
 * 获取终端名
 */
@Component
public interface TerminalService {
    String getTerminalName();
}
1
2
3
4
5
6
7
8
9

# 5.2 增加短信验证码模块

这里使用阿里大鱼短信验证码的第三方API实现,需要在平台上开通服务及短信模板。

pom.xml 引入依赖:

        <!-- 阿里大于(大鱼)短信 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.1.1</version>
        </dependency>
1
2
3
4
5
6

application.properties 新增配置:

## aliyun sms config
aliyun.sms.accessKeyId=your_accessKeyId
aliyun.sms.accessKeySecret=your_accessKeySecret
aliyun.sms.domain=dysmsapi.aliyuncs.com
aliyun.sms.resend-interval=60
aliyun.sms.usable-timeout=300
aliyun.sms.signName=your_signName
aliyun.sms.loginTemplate=your_loginTemplate
aliyun.sms.registerTemplate=your_registerTemplate
aliyun.sms.changePasswordTemplate=your_changePasswordTemplate
aliyun.sms.apiVersion=2017-05-25
aliyun.sms.url=http://dysmsapi.aliyuncs.com
1
2
3
4
5
6
7
8
9
10
11
12

./config/AliMessageConfig.java

package com.yoyo.admin.common.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliMessageConfig {

    private String accessKeyId;
    private String accessKeySecret;
    private String domain;
    private String signName;
    private String loginTemplate;
    private String registerTemplate;
    private String changePasswordTemplate;
    private String breedTemplate;
    private String apiVersion;
    private Integer resendInterval;
    private Integer usableTimeout;
    private String url;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

./domain/ConfirmCodeMessage.java

package com.yoyo.admin.common.domain;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@ApiModel(value = "confirm_code_message", description = "阿里大于(大鱼)短信验证码表")
@Table(name = "confirm_code_message", indexes = {@Index(columnList = "phone_number")})
@org.hibernate.annotations.Table(appliesTo = "confirm_code_message", comment = "阿里大于(大鱼)短信验证码表")
public class ConfirmCodeMessage {

    /**
     * 主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @ApiModelProperty(value = "主键")
    @Column(name = "id", columnDefinition = "bigint COMMENT '主键'")
    private Long id;

    /**
     * 手机号码
     */
    @ApiModelProperty(value = "手机号码")
    @Column(name = "phone_number", columnDefinition = "varchar(255) COMMENT '手机号码'")
    private String phoneNumber;

    /**
     * 短信验证码
     */
    @ApiModelProperty(value = "短信验证码")
    @Column(name = "confirm_code", columnDefinition = "varchar(10) COMMENT '短信验证码'")
    private String confirmCode;

    /**
     * 是否使用
     */
    @ApiModelProperty(value = "是否使用 0:未使用 1:已使用")
    @Column(name = "is_used", columnDefinition = "int COMMENT '是否使用 0:未使用 1:已使用'")
    private Integer isUsed;

    /**
     * 创建时间
     */
    @CreationTimestamp
    @ApiModelProperty(value = "创建时间")
    @Column(name = "create_time", updatable = false, columnDefinition = "datetime COMMENT '创建时间'")
    private Date createTime;

    /**
     * 验证码类型
     */
    @ApiModelProperty(value = "验证码类型 1:注册 2:登录 3:修改密码")
    @Column(name = "type", columnDefinition = "int COMMENT '验证码类型 1:注册 2:登录 3:修改密码'")
    private Integer type;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

./domain/repository/ConfirmCodeMessagingRepository.java

package com.yoyo.admin.common.domain.repository;

import com.yoyo.admin.common.domain.ConfirmCodeMessage;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Date;

public interface ConfirmCodeMessagingRepository extends JpaRepository<ConfirmCodeMessage, Long> {

    /**
     * 根据手机号、创建时间、是否使用查询符合条件的记录
     * @param phoneNumber
     * @param maxDate
     * @param isUsed
     * @return
     */
    ConfirmCodeMessage findFirstByPhoneNumberAndCreateTimeAfterAndIsUsedOrderByIdDesc(String phoneNumber, Date maxDate, Integer isUsed);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

AliConfirmCodeMessageService.java

package com.yoyo.admin.common.service;

import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.yoyo.admin.common.config.AliMessageConfig;
import com.yoyo.admin.common.domain.ConfirmCodeMessage;
import com.yoyo.admin.common.domain.repository.ConfirmCodeMessagingRepository;
import com.yoyo.admin.common.logger.annotation.CreateLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Random;

/**
 * 阿里大于(大鱼)短信验证码
 */
@Service
@Slf4j
public class AliConfirmCodeMessageService {
    private ConfirmCodeMessagingRepository confirmCodeMessagingRepository;
    private AliMessageConfig aliMessageConfig;

    @Autowired
    public void setConfirmCodeMessagingRepository(ConfirmCodeMessagingRepository confirmCodeMessagingRepository) {
        this.confirmCodeMessagingRepository = confirmCodeMessagingRepository;
    }

    @Autowired
    public void setAliMessageConfig(AliMessageConfig aliMessageConfig) {
        this.aliMessageConfig = aliMessageConfig;
    }


    public ConfirmCodeMessage getForResend(String phoneNumber) {
        Date maxDate = new Date(System.currentTimeMillis() - aliMessageConfig.getResendInterval() * 1000);
        return confirmCodeMessagingRepository.findFirstByPhoneNumberAndCreateTimeAfterAndIsUsedOrderByIdDesc(phoneNumber, maxDate, 0);
    }

    public ConfirmCodeMessage create(String phoneNumber, String template, Integer type) throws Exception {
        ConfirmCodeMessage confirmCodeMessage = getForResend(phoneNumber);
        if (confirmCodeMessage != null) {
            throw new Exception("可重发时间未到");
        }
        Random random = new Random();
        String code = String.format("%04d", random.nextInt(9999));
        // 短信平台发送的验证码如果开头是0, 会省略
        if ("0".equals(code.substring(0, 1))) {
            code = "6" + code.substring(1);
        }
        confirmCodeMessage = new ConfirmCodeMessage();
        confirmCodeMessage.setPhoneNumber(phoneNumber);
        confirmCodeMessage.setConfirmCode(code);
        confirmCodeMessage.setIsUsed(0);
        confirmCodeMessage.setType(type);
        confirmCodeMessage.setCreateTime(new Date());
        JSONObject response = JSONObject.parseObject(sendMessage(phoneNumber, code, template));
        if (response.get("Message") != null && "OK".equals(response.get("Code"))) {
            confirmCodeMessagingRepository.save(confirmCodeMessage);
            return confirmCodeMessage;
        } else {
            log.error(response.toJSONString());
            throw new Exception("短信发送失败");
        }
    }

    /**
     * 发送短信验证码
     * @param phone
     * @param code
     * @param template
     * @return
     * @throws Exception
     */
    private String sendMessage(String phone, String code, String template) throws Exception {
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", aliMessageConfig.getAccessKeyId(),
                aliMessageConfig.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);
        CommonRequest request = new CommonRequest();
        request.setMethod(MethodType.POST);
        request.setDomain(aliMessageConfig.getDomain());
        request.setVersion(aliMessageConfig.getApiVersion());
        request.setAction("SendSms");
        request.putQueryParameter("PhoneNumbers", phone);
        request.putQueryParameter("TemplateCode", template);
        request.putQueryParameter("SignName", aliMessageConfig.getSignName());
        request.putQueryParameter("TemplateParam", "{\"code\":" + code + "}");
        CommonResponse response = null;
        try {
            response = client.getCommonResponse(request);
        } catch (ClientException e) {
            e.printStackTrace();
            log.error(e.getMessage());
            throw new Exception("短信发送失败");
        }
        if (response != null && response.getHttpResponse().isSuccess()) {
            return response.getData();
        } else {
            throw new Exception("短信发送失败");
        }
    }

    @CreateLog(target = "验证码", operation = "发送注册验证码")
    public ConfirmCodeMessage createForRegister(String phoneNumber) throws Exception {
        return create(phoneNumber, aliMessageConfig.getRegisterTemplate(), 1);
    }

    @CreateLog(target = "验证码", operation = "发送登录验证码")
    public ConfirmCodeMessage createForLogin(String phoneNumber) throws Exception {
        return create(phoneNumber, aliMessageConfig.getLoginTemplate(), 2);
    }

    @CreateLog(target = "验证码", operation = "发送修改密码验证码")
    public ConfirmCodeMessage createForChangePassword(String phoneNumber) throws Exception {
        return create(phoneNumber, aliMessageConfig.getChangePasswordTemplate(), 3);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

ConfirmCodeController.java

package com.yoyo.admin.web_manage.controller;

import cn.hutool.core.util.StrUtil;
import com.yoyo.admin.common.domain.ConfirmCodeMessage;
import com.yoyo.admin.common.domain.User;
import com.yoyo.admin.common.service.AliConfirmCodeMessageService;
import com.yoyo.admin.common.service.UserService;
import com.yoyo.admin.common.utils.ResultDataUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;


@Api(tags = "验证码发送")
@RestController
@RequestMapping("/auth")
public class ConfirmCodeController {

    private AliConfirmCodeMessageService aliConfirmCodeMessageService;

    @Autowired
    public void setAliConfirmCodeMessageService(AliConfirmCodeMessageService aliConfirmCodeMessageService) {
        this.aliConfirmCodeMessageService = aliConfirmCodeMessageService;
    }

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }


    @ApiOperation("注册发送验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "phoneNumber", value = "手机号码", dataType = "String", paramType = "query"),
    })
    @PostMapping(value = "/register/confirmCode")
    public ResponseEntity<?> sendConfirmCodeForRegister(@RequestParam String phoneNumber) {
        if (StrUtil.isEmpty(phoneNumber)) {
            return ResultDataUtils.error("请输入手机号码");
        }
        User user = userService.getUserByPhoneNumber(phoneNumber);
        if (user != null) {
            return ResultDataUtils.error("手机号码已注册");
        }
        try {
            ConfirmCodeMessage confirmCodeMessage = aliConfirmCodeMessageService.createForRegister(phoneNumber);
            Map<String, Long> map = new HashMap<>();
            map.put("sendTime", confirmCodeMessage.getCreateTime().getTime());
            return ResultDataUtils.success(map);
        } catch (Exception ex) {
            ex.printStackTrace();
            return ResultDataUtils.error(ex.getMessage());
        }
    }

    @ApiOperation("登录发送验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "phoneNumber", value = "手机号码", dataType = "String", paramType = "query"),
    })
    @PostMapping(value = "/login/confirmCode")
    public ResponseEntity<?> sendConfirmCodeForLogin(@RequestParam String phoneNumber) {
        if (StrUtil.isEmpty(phoneNumber)) {
            return ResultDataUtils.error("请输入手机号码");
        }
        User user = userService.getUserByPhoneNumber(phoneNumber);
        if (user == null) {
            return ResultDataUtils.error("手机号码未注册");
        }
        try {
            ConfirmCodeMessage confirmCodeMessage = aliConfirmCodeMessageService.createForLogin(phoneNumber);
            Map<String, Long> map = new HashMap<>();
            map.put("sendTime", confirmCodeMessage.getCreateTime().getTime());
            return ResultDataUtils.success(map);
        } catch (Exception ex) {
            ex.printStackTrace();
            return ResultDataUtils.error(ex.getMessage());
        }
    }

    @ApiOperation("修改密码发送验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "phoneNumber", value = "手机号码", dataType = "String", paramType = "query"),
    })
    @PostMapping(value = "/changePassword/confirmCode")
    public ResponseEntity<?> sendConfirmCodeForChangePassword(@RequestParam String phoneNumber) {
        if (StrUtil.isEmpty(phoneNumber)) {
            return ResultDataUtils.error("请输入手机号码");
        }
        User user = userService.getUserByPhoneNumber(phoneNumber);
        if (user == null) {
            return ResultDataUtils.error("手机号码未注册");
        }
        try {
            ConfirmCodeMessage confirmCodeMessage = aliConfirmCodeMessageService.createForChangePassword(phoneNumber);
            Map<String, Long> map = new HashMap<>();
            map.put("sendTime", confirmCodeMessage.getCreateTime().getTime());
            return ResultDataUtils.success(map);
        } catch (Exception ex) {
            ex.printStackTrace();
            return ResultDataUtils.error(ex.getMessage());
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

# 6. 四种常规的登录认证方式

以下介绍4种常规的登录认证方式,每个方案各自的使用场景如下:

  • Cookie + Session 历史悠久,适合于简单的后端架构,需开发人员自己处理好安全问题。
  • Token 方案对后端压力小,适合大型分布式的后端架构,但已分发出去的 Token ,如果想收回权限,就不是很方便了。
  • SSO 单点登录,适用于中大型企业,想要统一内部所有产品的登录方式。
  • OAuth 第三方登录,简单易用,对用户和开发者都友好,但第三方平台很多,需要选择合适自己的第三方登录平台。

HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态。为了解决 HTTP 无状态的问题,Lou Montulli 在 1994 年的时候,推出了 Cookie。

Cookie 是服务器端发送给客户端的一段特殊信息,这些信息以文本的方式存放在客户端,客户端每次向服务器端发送请求时都会带上这些特殊信息。

当访问一个页面的时候,服务器在下行http报文中,命令浏览器存储一个字符串;浏览器再访问同一个域的时候,将把这个字符串携带到上行http请求中。第一次访问一个服务器,不可能携带Cookie 。必须是服务器得到这次请求,在下行响应报头中,携带Cookie 信息,此后每一次浏览器往这个服务器发出的请求,都会携带这个Cookie 。有了 Cookie 之后,服务器端就能够获取到客户端传递过来的信息了,如果需要对信息进行验证,还需要通过 Session。

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个便是 Session 对象。

有了 Cookie 和 Session 之后,我们就可以进行登录认证了。

session在计算机网络应用中被称为“会话控制”。客户端浏览器访问网站的时候,服务器会向客户浏览器发送一个每个用户特有的会话编号sessionID,让他进入到cookie里,服务器同时也把sessionID和对应的用户信息、用户操作记录在服务器上,这些记录就是session。客户端浏览器再次访问时,会发送cookie给服务器,其中就包含sessionID。服务器从cookie里找到sessionID,再根据sessionID找到以前记录的用户信息就可以知道他之前操控些、访问过哪里。

Cookie + Session 的登录方式是最经典的一种登录方式,现在仍然有大量的企业在使用。

用户首次登录时:

Cookie+Session实现流程-1
  1. 用户访问 a.com/pageA,并输入密码登录。
  2. 服务器验证密码无误后,会创建 SessionId,并将它保存起来。
  3. 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。
  4. 浏览器会根据Set-Cookie中的信息,自动将SessionId存储至cookie中。

服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。

第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:

Cookie+Session实现流程-2
  1. 用户访问 a.com/pageB 页面时,会自动带上第一次登录时写入的 Cookie。
  2. 服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。
  3. 如果一致,则身份验证成功。

虽然我们使用 Cookie + Session 的方式完成了登录验证,但仍然存在一些问题:

  • 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。
  • 如果服务器端是一个集群,为了同步登录态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端维护成本。
  • 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击。

# 6.2 Token 登录

为了解决 Session + Cookie 机制暴露出的诸多问题,我们可以使用 Token 的登录方式。

Token是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

# 6.2.1 Token 机制实现流程

用户首次登录时:

Token机制实现流程-1
  1. 用户输入账号密码,并点击登录。
  2. 服务器端验证账号密码无误,创建 Token。
  3. 服务器端将 Token 返回给客户端,由客户端自由保存。

后续页面访问时:

Token机制实现流程-2
  1. 用户访问 a.com/pageB 时,带上第一次登录时获取的 Token。
  2. 服务器端验证 Token ,有效则身份验证成功。

# 6.2.2 Token 机制的特点

根据上面的案例,我们可以分析出 Token 的优缺点:

  • 服务器端不需要存放 Token,所以不会对服务器端造成压力,即使是服务器集群,也不需要增加维护成本。
  • Token 可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。
  • Token 下发之后,只要在生效时间之内,就一直有效,如果服务器端想收回此 Token 的权限,并不容易。

# 6.2.3 Token 的生成方式

最常见的 Token 生成方式是使用 JWT(Json Web Token),它是一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。但上文说到,使用 Token 后,服务器端并不会存储 Token,那怎么判断客户端发过来的 Token 是合法有效的呢?

答案其实就在 Token 字符串中,其实 Token 并不是一串杂乱无章的字符串,而是通过多种算法拼接组合而成的字符串,我们来具体分析一下。

JWT 算法主要分为 3 个部分:header(头信息),playload(消息体),signature(签名)。

header 部分指定了该 JWT 使用的签名算法:

header = '{"alg":"HS256","typ":"JWT"}' // `HS256` 表示使用了 HMAC-SHA256 来生成签名。
1

playload 部分表明了 JWT 的意图:

payload = '{"loggedInAs":"admin","iat":1422779638}'  //iat 表示令牌生成的时间
1

signature 部分为 JWT 的签名,主要为了让 JWT 不能被随意篡改,签名的方法分为两个步骤:

  1. 输入 base64url 编码的 header 部分 、base64url 编码的 playload 部分,输出 unsignedToken。
  2. 输入服务器端私钥、unsignedToken,输出 signature 签名。
const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const unsignedToken = `${base64Header}.${base64Payload}`
const key = '服务器私钥'

signature = HMAC(key, unsignedToken)
最后的 Token 计算如下:
1
2
3
4
5
6
7
const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const base64Signature = encodeBase64(signature)

token = `${base64Header}.${base64Payload}.${base64Signature}` 
1
2
3
4
5

服务器在判断 Token 时:

const [base64Header, base64Payload, base64Signature] = token.split('.')

const signature1 = decodeBase64(base64Signature)
const unsignedToken = `${base64Header}.${base64Payload}`
const signature2 = HMAC('服务器私钥', unsignedToken)

if(signature1 === signature2) {
  return '签名验证成功,token 没有被篡改'
}

const payload =  decodeBase64(base64Payload)
if(new Date() - payload.iat < 'token 有效期'){
  return 'token 有效'
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

有了 Token 之后,登录方式已经变得非常高效。

# 6.3 SSO 单点登录

单点登录指的是在公司内部搭建一个公共的认证中心,公司下的所有产品的登录都可以在认证中心里完成,一个产品在认证中心登录后,再去访问另一个产品,可以不用再次登录,即可获取登录状态。

# 6.3.1 SSO 机制实现流程

用户首次访问时,需要在认证中心登录:

SSO机制实现流程-1
  1. 用户访问网站 a.com 下的 pageA 页面。
  2. 由于没有登录,则会重定向到认证中心,并带上回调地址 www.sso.com?return_uri=a.com/pageA,以便登录后直接进入对应页面。
  3. 用户在认证中心输入账号密码,提交登录。
  4. 认证中心验证账号密码有效,然后重定向 a.com?ticket=123 带上授权码 ticket,并将认证中心 sso.com 的登录态写入 Cookie。
  5. a.com 服务器中,拿着 ticket 向认证中心确认,授权码 ticket 真实有效。
  6. 验证成功后,服务器将登录信息写入 Cookie(此时客户端有 2 个 Cookie 分别存有 a.comsso.com 的登录态)。

认证中心登录完成之后,继续访问 a.com 下的其他页面:

SSO机制实现流程-2

这个时候,由于 a.com 存在已登录的 Cookie 信息,所以服务器端直接认证成功。

如果认证中心登录完成之后,访问 b.com 下的页面:

SSO机制实现流程-3

这个时候,由于认证中心存在之前登录过的 Cookie,所以也不用再次输入账号密码,直接返回第 4 步,下发 ticket 给 b.com 即可。

# 6.3.2 SSO 单点登录退出

目前我们已经完成了单点登录,在同一套认证中心的管理下,多个产品可以共享登录态。现在我们需要考虑退出了,即:在一个产品中退出了登录,怎么让其他的产品也都退出登录?原理其实不难,可以回过头来看第 5 步,每一个产品在向认证中心验证 ticket 时,其实可以顺带将自己的退出登录 api 发送到认证中心。

当某个产品 c.com 退出登录时:

  1. 清空 c.com 中的登录态 Cookie。
  2. 请求认证中心 sso.com 中的退出 api。
  3. 认证中心遍历下发过 ticket 的所有产品,并调用对应的退出 api,完成退出。

# 6.4 OAuth 第三方登录

在上文中,我们使用单点登录完成了多产品的登录态共享,但都是建立在一套统一的认证中心下,对于一些小型企业,未免太麻烦,有没有一种登录能够做到开箱即用?其实是有的,很多大厂都会提供自己的第三方登录服务,比如QQ登录、微信登录、Google登录、Github登录...

这里以微信开放平台的接入流程为例:

OAuth机制实现流程
  1. 首先,a.com 的运营者需要在微信开放平台注册账号,并向微信申请使用微信登录功能。
  2. 申请成功后,得到申请的 appid、appsecret。
  3. 用户在 a.com 上选择使用微信登录。
  4. 这时会跳转微信的 OAuth 授权登录,并带上 a.com 的回调地址。
  5. 用户输入微信账号和密码,登录成功后,需要选择具体的授权范围,如:授权用户的头像、昵称等。
  6. 授权之后,微信会根据拉起 a.com?code=123 ,这时带上了一个临时票据 code。
  7. 获取 code 之后, a.com 会拿着 code 、appid、appsecret,向微信服务器申请 token,验证成功后,微信会下发一个 token。
  8. 有了 token 之后, a.com 就可以凭借 token 拿到对应的微信用户头像,用户昵称等信息了。
  9. a.com 提示用户登录成功,并将登录状态写入 Cookie,以作为后续访问的凭证。

# 7. Java和Python加密源码打包

需求情景:有些项目交付时,只是让对方可以私有化部署,但不交付源码。该情形下,打包时可以对其进行加密混淆,或者将其进行编译,可避免源码泄漏或字节码被反编译。

# 7.1 Jar包加密打包

ClassFinal是一款java class文件加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring-framework,可避免源码泄漏或字节码被反编译。

使用方法: 使用如下命令对jar包进行加密即可,生成的 yourproject-encrypted.jar 就是加密后的jar文件,加密后的文件不可直接执行,需要配置javaagent。

$ java -jar classfinal-fatjar.jar -file yourproject.jar -libjars a.jar,b.jar -packages com.yourpackage,com.yourpackage2 -exclude com.yourpackage.Main -pwd 123456 -Y

参数说明
-file        加密的jar/war完整路径
-packages    加密的包名(可为空,多个用","分割)
-libjars     jar/war包lib下要加密jar文件名(可为空,多个用","分割)
-cfgfiles    需要加密的配置文件,一般是classes目录下的yml或properties文件(可为空,多个用","分割)
-exclude     排除的类名(可为空,多个用","分割)
-classpath   外部依赖的jar目录,例如/tomcat/lib(可为空,多个用","分割)
-pwd         加密密码,如果是#号,则使用无密码模式加密
-code        机器码,在绑定的机器生成,加密后只可在此机器上运行
-Y           无需确认,不加此参数会提示确认以上信息
1
2
3
4
5
6
7
8
9
10
11
12

使用实例:加密后的项目需要设置javaagent来启动,项目在启动过程中解密class,完全内存解密,不留下任何解密后的文件。

$ java -jar classfinal-fatjar-1.2.1.jar -file web_manage-0.0.1.jar -packages com.yoyo.admin.web_manage -pwd 123456 -Y // 对Jar加密
$ java -javaagent:web_manage-0.0.1-encrypted.jar='-pwd 123456’ -jar web_manage-0.0.1-encrypted.jar   // 启动加密Jar
1
2

如果将jar包解压出来查看源码,会发现里面的具体实现全都没了,拿不到多少有用的信息。

ClassFinal加密后的效果

# 7.2 Python源码加密打包

编译动态链接库的方式,不会被逆向破解,但是由于Cython的编译要求严格,可能需要改源码,而且有些复杂的项目编译后的引用会出问题。源码混淆的方式,容易被逆向破解,但复杂项目里不太会出问题(命名替换什么的就算了,一堆文件之间相互关联,都没换成同一个,到处都是爆红),也更容易适配系统差异。

# 7.2.1 Python编译动态链接库

可以将 Python 代码编译成动态链接库,这样就不会被逆向破解拿到源码了,需要安装Cython库。

$ pip3 install Cython
1

compile.py

# -*- coding: utf-8 -*-

import os
import shutil
import tempfile
from setuptools import setup, Extension
from Cython.Build import cythonize

# 指定你的源文件目录
source_dir = './fast-text-rank'
output_dir = source_dir + '_build'

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 创建临时目录
with tempfile.TemporaryDirectory() as temp_dir:
    temp_source_dir = os.path.join(temp_dir, 'src')

    # 递归复制文件夹并保留目录结构
    shutil.copytree(source_dir, temp_source_dir, ignore=shutil.ignore_patterns('*.pyc', '__pycache__'))

    # 找到所有的 .py 文件并创建一个扩展对象列表
    extensions = []
    for root, dirs, files in os.walk(temp_source_dir):
        for filename in files:
            if filename.endswith('.py'):
                abs_file_path = os.path.join(root, filename)
                relative_file_path = os.path.relpath(abs_file_path, temp_source_dir)
                module_name = os.path.splitext(relative_file_path.replace(os.path.sep, '.'))[0]
                pyx_file_path = abs_file_path + 'x'  # Append 'x' to make .pyx extension
                os.rename(abs_file_path, pyx_file_path)
                extensions.append(Extension(module_name, [pyx_file_path]))

    # 编写 setup.py 文件内容并构建
    setup(
        ext_modules=cythonize(extensions, compiler_directives={'language_level': "3"}),
        script_args=["build_ext", "--build-lib", output_dir],
    )

    # 复制临时目录中编译生成的文件到输出目录
    for root, dirs, files in os.walk(temp_dir):
        for filename in files:
            if filename.endswith('.so') or filename.endswith('.pyd'):
                # 相对于临时源目录的路径
                relative_path = os.path.relpath(root, temp_source_dir)
                # 构建目标目录
                dest_dir = os.path.join(output_dir, relative_path)
                os.makedirs(dest_dir, exist_ok=True)
                shutil.move(os.path.join(root, filename), dest_dir)

# 拷贝源目录下的非 .py 文件到输出目录,并保留目录结构
for root, dirs, files in os.walk(source_dir):
    for item in dirs + files:
        src_path = os.path.join(root, item)
        dst_path = src_path.replace(source_dir, output_dir, 1)
        if os.path.isdir(src_path):
            os.makedirs(dst_path, exist_ok=True)
        elif not item.endswith('.py') and not item.endswith('.pyc') and not os.path.exists(dst_path):
            shutil.copy2(src_path, dst_path)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

适用情形:编译成动态链接库的 Python 代码不能直接执行,因为它们不是独立的可执行程序,因此需要一个主程序去调用。在原来主程序的同级目录新建一个main.py,引用一下原来主程序模块,写一下if __name__ == '__main__':即可,内容如下:

# -*- coding: utf-8 -*-

# 导入编译后的server模块
import server

# 从server模块中获取Flask实例并运行它
if __name__ == '__main__':
    server.app.run(host='0.0.0.0', port=4999, debug=False, threaded=True)
1
2
3
4
5
6
7
8

注意事项:如果原来主程序的if __name__ == '__main__':里写了需要外部依赖的代码,可以把原来这里的东西都写到 def main():里,统一成这种格式。

Python编译成动态链接库打包

# 7.2.2 Python混淆源码

Python混淆源码的库有很多,我几乎都试遍了,基本都是些玩具项目。简单项目用起来还行,项目复杂了之后,各个文件之前相互关联,混淆后就跑不起来了。

最后为了实现这个复杂项目的源码混淆,手写了一个脚本,主要包含压缩代码内容、添加垃圾代码、转换成一行显示等操作。

$ pip3 install python-minifier      //  Python源码压缩库:https://github.com/dflook/python-minifier
1

code_mixture.py

# -*- coding: utf-8 -*-

import os
import shutil
import logging
import re
import random
from python_minifier import minify

# 日志记录
logging.basicConfig(filename='code_minifier.log', level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


# 文件编码转换并添加运行时解码器
def encode_with_runtime_decoder(content):
    # 将代码转换为一个长字符串
    encoded_content = repr(content)
    # 创建一个字符串,其中包含了执行原始代码的逻辑
    decoder = f"exec({encoded_content})\n"
    return decoder


# 生成随机垃圾代码
def generate_garbage_code():
    patterns = [
        # 列表推导式生成随机列表
        f"[{random.randint(0, 100)} for _ in range({random.randint(1, 3)})]\n",
        # 随机生成一个整数,但不使用它
        f"_unused_var_{random.randint(100, 999)} = {random.randint(100, 999)}\n",
        # 使用条件语句,确保有一个有效的代码块
        f"if {random.randint(0, 1)} == 0: pass\n",
        # 生成一个随机的函数定义
        f"def _useless_function_{random.randint(100, 999)}(): pass\n",
        # 生成一个随机的 try-except 语句
        f"try:\n    pass\nexcept Exception as e:\n    pass\n",
        # 随机生成一个 lambda 表达式
        f"_lambda_{random.randint(100, 999)} = lambda x: x + {random.randint(0, 100)}\n",
        # 生成一个随机的字符串赋值
        f"_random_string_{random.randint(100, 999)} = 'string{random.randint(0, 100)}'\n",
        # 生成一个随机的字典赋值
        f"_random_dict_{random.randint(100, 999)} = {{'key': {random.randint(0, 100)}}}\n",
    ]
    # 生成多行垃圾代码,每行随机选择一种模式
    lines = [random.choice(patterns) for _ in range(random.randint(10, 100))]
    return ''.join(lines)


# 在源代码末尾插入垃圾代码
def insert_garbage_code_at_end(source_code):
    garbage_code = generate_garbage_code()
    return source_code + '\n' + garbage_code


def process_directory(source_dir, target_dir, whitelist=None, skip_patterns=None):
    logging.info("开始混淆处理过程")

    # 确保目标目录存在
    if not os.path.exists(target_dir):
        os.makedirs(target_dir)

    # 将白名单中的路径转换为绝对路径
    if whitelist is not None:
        whitelist = [os.path.abspath(path) for path in whitelist]

    # 编译跳过的正则表达式
    if skip_patterns is not None:
        skip_patterns = [re.compile(pattern) for pattern in skip_patterns]

    # 获取总文件数量
    total_files = sum([len(files) for r, d, files in os.walk(source_dir)])
    processed_files = 0

    # 遍历原始目录中的所有文件
    for root, dirs, files in os.walk(source_dir):
        for file in files:
            source_file = os.path.join(root, file)
            target_file = os.path.join(target_dir, os.path.relpath(source_file, source_dir))
            absolute_source_file = os.path.abspath(source_file)

            # 检查是否应该跳过文件
            if any(pattern.search(absolute_source_file) for pattern in skip_patterns):
                continue
            # 确保目标文件的目录存在
            os.makedirs(os.path.dirname(target_file), exist_ok=True)

            # 更新处理进度
            processed_files += 1
            logger.info(f"处理文件 {source_file} ({processed_files}/{total_files})")

            # 检查文件是否在白名单中或是否为Python文件
            if absolute_source_file in whitelist or not file.endswith(".py"):
                # 白名单中的文件或非Python文件,直接拷贝
                shutil.copy2(source_file, target_file)
            else:
                try:
                    with open(source_file, 'r') as f:
                        original_content = f.read()
                    # 添加垃圾代码
                    source_code_with_garbage = insert_garbage_code_at_end(original_content)
                    # 文件编码转换
                    content_with_decoder = encode_with_runtime_decoder(source_code_with_garbage)
                    # 代码内容压缩
                    obfuscated_code = minify(content_with_decoder, remove_literal_statements=True)
                    with open(target_file, 'w') as f:
                        f.write(obfuscated_code)
                except Exception as e:
                    # 混淆过程中出现异常,直接拷贝文件
                    shutil.copy2(source_file, target_file)
                    logger.error(e)
                    logger.error(f"Failed to obfuscate {source_file}. File copied without obfuscation.")

    logger.info("混淆处理过程完成。")


if __name__ == '__main__':
    source_dir = "/root/your_project"
    target_dir = "/root/your_project_mixture"
    # 白名单,跳过混淆处理,拷贝到target_dir
    whitelist = [
        "{}{}".format(source_dir, "/test.py")
    ]
    # 放弃拷贝到target_dir,正则格式
    skip_patterns = [
        r"requirements(-.*)?\.txt",
        r".*__pycache__.*",
        r".*\.idea",
    ]
    process_directory(source_dir, target_dir, whitelist, skip_patterns)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

# 8. 系统部署安全注意事项

本节主要是与系统部署有关的安全防范,此处仅简要介绍,具体见我的其他博客:VPS基本部署环境的搭建与配置 (opens new window)Docker容器化及项目环境管理 (opens new window)

# 8.1 开启防火墙并设置合理的规则

开启防火墙,并配置合理的iptables,ufw防火墙规则来保证系统层面的安全,主要注意的是docker自带防火墙,可能会与系统防火墙冲突。除此之外,不放行非必要的端口,必要时要设置IP白名单与IP黑名单来进行限制也是非常有必要的。

ufw防火墙常用命令

$ sudo ufw enable      // 启动ufw防火墙
$ sudo ufw disable     // 关闭ufw防火墙
$ sudo ufw status      // 查看ufw防火墙状态
$ sudo ufw allow 443   // 允许外部访问443端口
$ sudo ufw deny 443    // 禁止外部访问443端口
1
2
3
4
5

# 8.2 使用安全版本的中间件

有些版本的中间件有安全漏洞,比如7.16.0之前的ES、1.20.0之前的Nginx,在部署中间件的时候要使用当下安全的版本,后续如果曝出安全漏洞,要及时升级,但也要注意不同版本的兼容性问题。

# 8.3 使用shell脚本对数据定期备份

对存储在MySQL、Oracle、ElasticSearch等数据库及中间件的业务数据,为了防止意外发生,应该在服务器上配置shell脚本,定期对数据进行备份。

具体详见我的另一篇博客:常用服务的数据备份及同步专题 (opens new window)

# 8.4 设置Docker Network

默认docker之间的网络不互通,如果需要其互相连接,则需要配置docker network。配置完之后,这些docker之间便可使用hostname直接本地访问,不对外提供服务,保证系统的安全性。

$ docker network create [network_name]                            // 创建网络
$ docker network ls                                               // 查看已创建的网络列表
$ docker network inspect [network_name]                           // 查看具体的网络详情
$ docker network connect [network_name] [CONTAINER ID/NAMES]      // 将容器加入网络,或者 docker run 时加 --network 进行指定
$ docker network disconnect [network_name] [CONTAINER ID/NAMES]   // 将容器移除网络
$ docker network rm [network_name]                                // 删除具体的网络
1
2
3
4
5
6

# 8.5 设置IP白名单限制访问

对于一些面向B端的保密项目,可以使用IP白名单策略限制访问。如果客户的IP不是固定的,可以考虑提供VPN的方式给他们连接。

# 8.6 申请SSL证书开启HTTPS

SSL证书是一种数字证书,用于加密从用户的浏览器发送到Web服务器的数据。 通过这种方式,发送的数据对于使用Wireshark等数据包嗅探器来拦截和窃听您的通信的黑客来说是安全的。

Chrome一直在推动https,所有的http协议网站被标记为不安全,如果再不对网站进行https改造的话,那么可能会对信任度造成一定的影响,所以说对一个面向用户的网站来说,开启https是非常有必要的。

# 8.7 开启CDN服务

CDN的全称是Content Delivery Network,即内容分发网络。 CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

通过使用 CDN 服务提供的全球节点,一方面可以提高网站响应速度和性能,节省源站资源;另一方面也可以保护站点抵御攻击,保证网站长期稳定在线。

# 9. 参考资料

[1] docker搭建elasticsearch6.8.7并开启x-pack认证 from 程序员宅基地 (opens new window)

[2] SpringBoot 关闭druid的页面 from CSDN (opens new window)

[3] AES的256位密钥加解密报 java.security.InvalidKeyException: Illegal key size or default parameters 异常的处理及处理工具 from CSDN (opens new window)

[4] spring security 使用 application/json 接收数据 from CSDN (opens new window)

[5 Spring Security之多次登录失败后账户锁定功能的实现 from CSDN (opens new window)

[6] Swagger2进阶:集成统一认证和SpringSecurity的登录接口 from 稀土掘金 (opens new window)

[7] swagger添加权限验证,swagger安全控制 from CSDN (opens new window)

[8] Java字段脱敏处理 from 稀土掘金 (opens new window)

[9] Nessus+AWVS---Docker破解版安装 from Mari0er's Blog (opens new window)

[10] Nessus忘记用户名密码怎么办 from CSDN (opens new window)

[11] nessus新建扫描任务后,显示500内部服务器错误 from Github Issues (opens new window)

[12] 使用Nessus进行漏洞扫描的过程 from Chris Chan's BLOG (opens new window)

[13] Nessus 主机漏洞扫描器安装、配置、使用 from 51CTO (opens new window)

[14] 网络传输数据加解密方案选择(RSA+AES)from CSDN (opens new window)

[15] 4种常规的登录认证方式 from segmentfault (opens new window)

[16] 什么是 Log4j 漏洞 from IBM (opens new window)

[17] 保护您的Python脚本,将其加密为.pye并在导入时解密 from Github (opens new window)

[18] 将Python源代码转换为其最紧凑的表示形式 from Github (opens new window)

Last Updated: 1/15/2024, 11:56:49 AM