设计一个安全的登录接口

1 安全风险

1.1 暴力破解登录

对于暴露在公网的系统,需要防暴露破解

  • 验证码

    目前OCR技术很成熟,图片验证码很难有效防止机器人,最好采用滑动验证码等一些安全等级高的验证码

  • 登录限制

    限制用户名,或限制IP,都有一些缺陷

  • 手机验证

    最好的方式,但短信服务需要花钱

1.2 中间人攻击获取密码

中间人攻击(man-in-the-middle attack, abbreviated to MITM):简单一点来说就是,A和B在通讯过程中,攻击者通过嗅探、拦截等方式获取或修改A和B的通讯内容。

  • 更换HTTPS协议
  • 加密传输
    对于比较重要的敏感数据,我们可以手动进行加密传输
    • 可以在客户端使用非对称加密,在服务端解密
    • 可以在客户端进行MD5,防止暴露明文,这个方式在服务端也无法获取明文

1.3 安全存储用户密码

用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码也十分重要

  • MD5摘要算法保护你的密码;MD5虽然不可逆,容易被反向查找
  • MD5+盐摘要算法保护用户的密码(推荐);MD5(MD5(password+salt))仍可破解,但增大了破解成本
  • Bcrypt

2 百度安全登录过程分析

  1. 输入账号
  2. 当输入框失去焦点时,触发账号异常检查接口
    判断此次账号是否异常,登录是否需要验证码或其他认证
  3. 输入密码,提交
    • 获取RSA公钥
    • 提交用户轨迹到服务器,服务器返回一些重要信息用于后面
    • 提交密码:使用RSA公钥加密 并配合第二步参数提交
    • 根据第三步的返回值跳转对应地址或者提示账号密码错误

3 RSA加密登录实现

3.1 RSA加密登录分析

  • 普通策略:使用RSA密钥对,前端保留公钥文件,服务器后端保留私钥文件

    每次登录时,前端使用公钥对密码进行加密,后端使用私钥解密,表面上很安全了,每次使用公钥加密后得到的字符串都是不一样的;但是只是不一样,并不是不能用!如果攻击者拿着这个用户名和公钥加密后的密码直接调用你的登录接口,一样能登录成功,并获得授权令牌。

  • 推荐策略:用户每次登录时,由服务端生成一个随机密钥对,将公钥返回给前端,私钥保存到服务器,并设置该密钥对的有效期(一般30分钟)

    用户登录的时候,前端对用户的明文密码进行公钥加密后传给后端,然后后端用私钥对密码进行解密,然后与数据库保存的密码进行匹配,登录成功之后,服务器立即清除这个密钥对。这样即使攻击者拦截到了加密后的密码,并用这个加密后的密码再次伪造登录,也无法成功。

3.1 后端

public class RsaKey {
    private String publicKey;
    private String privateKey;

    public String getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(String publicKey) {
        this.publicKey = publicKey;
    }

    public String getPrivateKey() {
        return privateKey;
    }

    public void setPrivateKey(String privateKey) {
        this.privateKey = privateKey;
    }
}


public class RsaUtil {
    public static final String RSA_ALGORITHM = "RSA";

    /**
     * 创建RSA 公钥-私钥
     * @return
     */
    public static RsaKey createKeys(){
        return createKeys(1024);
    }

    /**
     * 创建RSA 公钥-私钥
     * @param keySize
     * @return
     */
    public static RsaKey createKeys(int keySize){
        //为RSA算法创建一个KeyPairGenerator对象
        KeyPairGenerator kpg = null;
        try{
            kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        }catch(NoSuchAlgorithmException e){
            e.printStackTrace();
        }

        //初始化KeyPairGenerator对象,密钥长度
        kpg.initialize(keySize);
        //生成密匙对
        KeyPair keyPair = kpg.generateKeyPair();

        //得到公钥
        Key publicKey = keyPair.getPublic();
        String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());

        //得到私钥
        Key privateKey = keyPair.getPrivate();
        String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());

        RsaKey rsaKey = new RsaKey();
        rsaKey.setPublicKey(publicKeyStr);
        rsaKey.setPrivateKey(privateKeyStr);

        return rsaKey;
    }


    /**
     * 公钥加密
     * @param originalText 原文
     * @param publicKey
     * @return
     */
    public static String publicEncrypt(String originalText, String publicKey){
        RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
        try{
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
            return Base64.encodeBase64String(cipher.doFinal(originalText.getBytes(StandardCharsets.UTF_8)));
        }catch(Exception e){
            throw new RuntimeException("加密字符串[" + originalText + "]时遇到异常", e);
        }
    }

    /**
     * 公钥解密
     * @param cipherText 密文
     * @param publicKey
     * @return
     */
    public static String publicDecrypt(String cipherText, String publicKey){
        RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
        try{
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, rsaPublicKey);
            return Base64.encodeBase64URLSafeString(cipher.doFinal(cipherText.getBytes(StandardCharsets.UTF_8)));
        }catch(Exception e){
            throw new RuntimeException("解密字符串[" + cipherText + "]时遇到异常", e);
        }
    }

    /**
     * 私钥加密
     * @param originalText 原文
     * @param privateKey
     * @return
     */
    public static String privateEncrypt(String originalText, String privateKey){
        RSAPrivateKey rsaPrivateKey=  getPrivateKey(privateKey);

        try{
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, rsaPrivateKey);
            return Base64.encodeBase64URLSafeString(cipher.doFinal(originalText.getBytes(StandardCharsets.UTF_8)));
        }catch(Exception e){
            throw new RuntimeException("加密字符串[" + originalText + "]时遇到异常", e);
        }
    }


    /**
     * 私钥解密
     * @param cipherText 密文
     * @param privateKey
     * @return
     */
    public static String privateDecrypt(String cipherText, String privateKey){
        RSAPrivateKey rsaPrivateKey=  getPrivateKey(privateKey);
        byte[] cipherBytes = Base64.decodeBase64(cipherText);
        try{
            Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
            return new String(cipher.doFinal(cipherBytes), StandardCharsets.UTF_8);
        }catch(Exception e){
            throw new RuntimeException("解密字符串[" + cipherText + "]时遇到异常", e);
        }
    }

    /**
     * 得到公钥
     * @param publicKey 密钥字符串(经过base64编码)
     * @throws Exception
     */
    private static RSAPublicKey getPublicKey(String publicKey) {
        //通过X509编码的Key指令获得公钥对象
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
        RSAPublicKey key = null;
        try {
            key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return key;
    }

    /**
     * 得到私钥
     * @param privateKey 密钥字符串(经过base64编码)
     * @throws Exception
     */
    private static RSAPrivateKey getPrivateKey(String privateKey){
        //通过PKCS#8编码的Key指令获得私钥对象
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        RSAPrivateKey key = null;
        try {
            key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return key;
    }

    public static void main(String[] args) {
        RsaKey rsaKey = RsaUtil.createKeys();

        System.out.println("公钥" );
        System.out.println(rsaKey.getPublicKey());

        System.out.println("私钥");
        System.out.println(rsaKey.getPrivateKey());

        System.out.println("公钥加密——私钥解密");

        String str = "zhang ying";
        System.out.println("明文:" + str);

        //公钥加密
        String encodedData = RsaUtil.publicEncrypt(str, rsaKey.getPublicKey());
        System.out.println("公钥加密后密文:" + encodedData);

        //私钥解密
        String decodedData = RsaUtil.privateDecrypt(encodedData, rsaKey.getPrivateKey());
        System.out.println("私钥解密后文字: " + decodedData);
    }
}

// 调用示例:用私钥解密用户名,密码
username = RsaUtil.privateDecrypt(username, rsaKey.getPrivateKey());
password = RsaUtil.privateDecrypt(password, rsaKey.getPrivateKey());

3.2 前端

<script th:src="@{/js/jsencrypt.js}"></script>

<script>

$(function () {
    $("#loginForm").submit(function (){
	// Encrypt with the public key...
	var encrypt = new JSEncrypt();
	encrypt.setPublicKey($('#publicKey').text());
	var username = encrypt.encrypt($('#username').val());
	var password = encrypt.encrypt($('#password').val());
	$('#username').val(username);
	$('#password').val(password);
	return true;
    });
});
</script>

参考资料


原文地址:https://www.cnblogs.com/hello-zy/p/15070886.html