4.1.ByteOJ用户模块——登录注册功能(RSA + TimeStamp加密过)

一、基础的登录注册功能实现逻辑

​ 注册登录功能的基本实现逻辑其实十分简单,莫过于前端发送账号、密码、验证码 等信息,随后后端对发送的信息进行一定的校验,比如:账号是否小于6位,密码是否少于8位,如果账号是特定的手机号或者QQ邮箱 ,可能后端还需要进行正则表达式的校验实现。

​ 像这种最简单的实现逻辑,我在这里就不过多赘述了。下面我将介绍我自己琢磨出来的(说实话,如果有人真的有意图想搞爬虫、或者对网站的请求进行拦截抓包,那么我的这种方案还是有很大漏洞的)。

​ 我下面将介绍的登录,实际上仅仅是结合了RSA + TimeStamp时间戳 + 低程度混淆加密 的方式来进行登录工作的,如下效果所示:

优点:不和传统的RSA加密方式完全一样,可以在一定程度上减少抓包对网站的影响,加密之后的密码只可在设定的规定时间内有效.

二、RSA加密

  1. 先说一下RSA加密的优点:这种加密方式属于非对称加密,在这里面它的最大特有两个概念,一个是公钥,还有一个是私钥,这两个密钥其实是同时产生的,一个用来加密,而另一个用来解密,公钥就是每个人都可以拿到的,你可以使用它对需要发送的信息进行加密,但是无法进行解密。然后我们想要对信息进行解密,我们就必须使用后端生成的私钥,不然基本可以说无法解密
  2. 前端首先向后端请求获取一个公钥,紧接着我们在使用这个jsencrpt拿到的公钥对这些账号啊,密码呀还有Token进行一个RSA加密
  3. 后端接收到加密后的信息,再使用后端所给的密钥进行解密
  4. 最后我们就可以拿到我们真实发送的数据了O(∩_∩)O哈哈~

三、RSA + Timstamp加密 + 混淆(混淆思路其实就是做一些小小的骚操作,可以说仅仅是个反爬处理)

​ **思考:**现在我们首先确认我们要解决的问题:防止黑客拿到我们的登录加密信息即可直接登录账户。但是左思右想我们自己发送的方式就是通过发送这些信息来进行校验密码的呀,还有什么好的方式吗,答案是Of Course,我们只需要给我们的校验信息加上一个时间,然后后端比较当前时间和这个给定的时间相差大小规定一个比较小的范围不OK了吗,同时也不用担心抓包问题了,因为黑客拿到这条抓包信息的时刻还是有些时间的吧,所以我们规定1min失效是不是蛮合理的O(∩_∩)O~~!!!即使他拿到了,因为无法解密修改时间戳,所以应该是没有太大问题的。

后期思考:虽然他拿不到真实的密码,但是黑客仍然可以拦截获取密码并获取Cookie信息。(可尝试使用类似于单例模式的思路实现加密密码仅支持一次登录,后期思考实现逻辑..............) 。这些都是我自己思考的,是否存在什么技术上的漏洞或者缺陷并不是很清楚,如有问题,请帮我即使斧正一下。

1.前端安裝下载

bash 复制代码
$ yarn add jsencrypt

2.使用例子(前端)

ts 复制代码
const userLogin = async () => {
  let encrypt = new JSEncrypt();

  // 获取公钥
  const resPublicGet = await RsaControllerService.getPublicKeyUsingGet();
  if (resPublicGet.code === 0) {
    encrypt.setPublicKey(resPublicGet.data);
  } else {
    ElNotification.success({
      title: "公钥获取失败",
      showClose: false,
    });
    return;
  }

  if (encrypt.getPublicKey() != null) {
    const res = await UserControllerService.userLoginUsingPost({
      account: encrypt.encrypt(userAccount.value),
      password: encrypt.encrypt(userPassword.value),
      token: encrypt.encrypt(token.value),
    });

    if (res.code === 0) {
      ElNotification.success({
        title: "登录成功",
        showClose: false,
      });
      router.replace("/");
    } else {
      ElNotification.error({
        title: "登录失败",
        message: res.message,
        showClose: false,
      });
    }
  }
};

3.后端引包

java 复制代码
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

4.后端实例(后端)

Controller层(获取公钥)

java 复制代码
package com.example.backend.controller;

import com.example.backend.common.BaseResponse;
import com.example.backend.common.ResultUtils;
import com.example.backend.utils.RSAUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RSAController {
 
    @RequestMapping("/getPublicKey")
    public BaseResponse<String> getPublicKey(){
        return ResultUtils.success(RSAUtil.getPublicKey());
    }
}

Controller(登录逻辑,例子,注册同理)

java 复制代码
@PostMapping("/login")
    private BaseResponse<UserVo> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest httpServletRequest) {
        if (userLoginRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "信息不能为空");
        }
        String Account = userLoginRequest.getAccount();
        String Password = userLoginRequest.getPassword();

        try {
            Account = RSAUtil.decryptWithPrivate(Account);
            Password = RSAUtil.decryptWithPrivate(Password);

            assert Password != null;
            // 获取当前时间的 Instant 对象
            Instant now = Instant.now();

            // 创建时间戳对应的 Instant 对象
            assert Account != null;
            Instant timestampInstant_1 = Instant.ofEpochMilli(Long.parseLong(Account.split(":")[1]));
            Instant timestampInstant_2 = Instant.ofEpochMilli(Long.parseLong(Password.split(":")[1]));

            // 计算时间差
            Duration duration_1 = Duration.between(timestampInstant_1, now);
            Duration duration_2 = Duration.between(timestampInstant_2, now);

            // 获取相差的秒数
            if (Math.abs(duration_1.getSeconds()) >= 60 || Math.abs(duration_2.getSeconds()) >= 60) {
                throw new BusinessException(ErrorCode.NOT_AUTH_ERROR, "小伙子你还挺狂,敢抓我包?????");
            }

            Password = Password.split(":")[0];
            // 这里就是解密后的信息了
            Account = Account.split(":")[0];
            Password = Password.split(":")[0];
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.NOT_AUTH_ERROR, "小伙子你还挺狂,敢抓我包????");
        }

        UserVo result = userService.UserLogin(Account, Password, httpServletRequest);
        return ResultUtils.success(result);
    }

RSA加密

java 复制代码
package com.example.backend.utils;

import com.alibaba.nacos.common.codec.Base64;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;

public class RSAUtil {
 
    //秘钥大小
    private static final int KEY_SIZE = 1024;
 
    //后续放到常量类中去
    public static final String PRIVATE_KEY = "xxxxxxxxx";
    public static final String PUBLIC_KEY = "xxxxxxxxx";
 
    private static KeyPair keyPair;
 
    private static Map<String, String> rsaMap;
 
    private static org.bouncycastle.jce.provider.BouncyCastleProvider bouncyCastleProvider = null;
 
    //BouncyCastleProvider内的方法都为静态方法,GC不会回收
    public static synchronized org.bouncycastle.jce.provider.BouncyCastleProvider getInstance() {
        if (bouncyCastleProvider == null) {
            bouncyCastleProvider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
        }
        return bouncyCastleProvider;
    }
 
    //生成RSA,并存放
    static {
        try {
            //通过以下方法,将每次New一个BouncyCastleProvider,可能导致的内存泄漏
   /*         Provider provider =new org.bouncycastle.jce.provider.BouncyCastleProvider();
            Security.addProvider(provider);
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);*/
            //解决方案
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getInstance());
            SecureRandom random = new SecureRandom();
            generator.initialize(KEY_SIZE, random);
            keyPair = generator.generateKeyPair();
            //将公钥和私钥存放,登录时会不断请求获取公钥
            //建议放到redis的缓存中,避免在分布式场景中,出现拿着server1的公钥去server2解密的问题
            storeRSA();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 将RSA存入缓存
     */
    private static void storeRSA() {
        rsaMap = new HashMap<>();
        PublicKey publicKey = keyPair.getPublic();
        String publicKeyStr = new String(Base64.encodeBase64(publicKey.getEncoded()));
        rsaMap.put(PUBLIC_KEY, publicKeyStr);
 
        PrivateKey privateKey = keyPair.getPrivate();
        String privateKeyStr = new String(Base64.encodeBase64(privateKey.getEncoded()));
        rsaMap.put(PRIVATE_KEY, privateKeyStr);
    }
 
    /**
     * 私钥解密(解密前台公钥加密的密文)
     *
     * @param encryptText 公钥加密的数据
     * @return 私钥解密出来的数据
     * @throws Exception e
     */
    public static String decryptWithPrivate(String encryptText) throws Exception {
        if (StringUtils.isBlank(encryptText)) {
            return null;
        }
        byte[] en_byte = Base64.decodeBase64(encryptText.getBytes());
        //可能导致内存泄漏问题
     /*   Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
        Security.addProvider(provider);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);*/
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", getInstance());
        PrivateKey privateKey = keyPair.getPrivate();
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] res = cipher.doFinal(en_byte);
        return new String(res);
    }
 
    /**
     * java端 使用公钥加密(此方法暂时用不到)
     *
     * @param plaintext 明文内容
     * @return byte[]
     * @throws UnsupportedEncodingException e
     */
    public static byte[] encrypt(String plaintext) throws UnsupportedEncodingException {
        String encode = URLEncoder.encode(plaintext, "utf-8");
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
        //获取公钥指数
        BigInteger e = rsaPublicKey.getPublicExponent();
        //获取公钥系数
        BigInteger n = rsaPublicKey.getModulus();
        //获取明文字节数组
        BigInteger m = new BigInteger(encode.getBytes());
        //进行明文加密
        BigInteger res = m.modPow(e, n);
        return res.toByteArray();
 
    }
 
    /**
     * java端 使用私钥解密(此方法暂时用不到)
     *
     * @param cipherText 加密后的字节数组
     * @return 解密后的数据
     * @throws UnsupportedEncodingException e
     */
    public static String decrypt(byte[] cipherText) throws UnsupportedEncodingException {
        RSAPrivateKey prk = (RSAPrivateKey) keyPair.getPrivate();
        // 获取私钥参数-指数/系数
        BigInteger d = prk.getPrivateExponent();
        BigInteger n = prk.getModulus();
        // 读取密文
        BigInteger c = new BigInteger(cipherText);
        // 进行解密
        BigInteger m = c.modPow(d, n);
        // 解密结果-字节数组
        byte[] mt = m.toByteArray();
        //转成String,此时是乱码
        String en = new String(mt);
        //再进行编码,最后返回解密后得到的明文
        return URLDecoder.decode(en, "UTF-8");
    }
 
    /**
     * 获取公钥
     *
     * @return 公钥
     */
    public static String getPublicKey() {
        return rsaMap.get(PUBLIC_KEY);
    }
 
    /**
     * 获取私钥
     *
     * @return 私钥
     */
    public static String getPrivateKey() {
        return rsaMap.get(PRIVATE_KEY);
    }
 
    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println(RSAUtil.getPrivateKey());
        System.out.println(RSAUtil.getPublicKey());
        byte[] usernames = RSAUtil.encrypt("username");
        System.out.println(RSAUtil.decrypt(usernames));
    }
}
相关推荐
想用offer打牌5 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX6 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法7 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端