1. 程式人生 > 其它 >SpringBoot通過RSA實現使用者名稱和密碼的加密和解密

SpringBoot通過RSA實現使用者名稱和密碼的加密和解密

前言

在我們輸入使用者名稱和密碼時,在傳輸的過程中應以加密的方式去傳遞到後臺,尤其是密碼,避免在登入的過程中,開啟瀏覽器的控制檯,便能輕鬆取得密碼。

一、RSA是什麼?

RSA 加密是一種 非對稱加密,可以在不直接傳遞金鑰的情況下,完成解密。這能夠確保資訊的安全性,避免了直接傳遞金鑰所造成的被破解的風險。是由一對金鑰來進行加解密的過程,分別稱為公鑰和私鑰。兩者之間有數學相關,該加密演算法的原理就是對一極大整數做因數分解的困難性來保證安全性。通常個人儲存私鑰,公鑰是公開的(可能同時多人持有)。

通過 RSA 實現使用者密碼加密傳輸,核心思路

  • 點選登入,先請求後端,生成一對公私鑰,將公鑰返回給前臺
  • 前臺使用開源的 jsencrypt.js 對密碼進行加密,加密後傳輸到後臺
  • 後臺對加密的密碼進行解密

二、使用步驟

使用的是 sprngboot + thymeleaf 模板進行整合的。

1.前端:

1.1建立前端頁面:login.html,檔案位置templates/login.html

程式碼如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script src="./js/jsencrypt.js"></script>

<body>
<h1 >RSA測試</h1>
<form>
    使用者賬號:
    <input type="text" name="username" id="username">
    <br>
    使用者密碼:
    <input type="password" name="password" id="password">
    <br>
    <input type="button" th:onclick="login()" th:value="提交">
</form>
</body>
<script>
    function login() {
        var username = $('#username').val();
        var password = $('#password').val();

        var encrypt = new JSEncrypt();

        $.ajax({
            type: "get",  // 提交方式
            url: "/getPublicKey",// 訪問路徑
            contentType: 'application/json;charset=utf-8',//返回json結果
            success: function (data) {
                console.log(data);
                encrypt.setPublicKey(data);
                var encryptPwd = encrypt.encrypt(password);
                var encryptUsername = encrypt.encrypt(username);
                console.log("encryptPwd:"+encryptPwd)
                $.ajax({
                    type: "post",  //提交方式
                    url: "/loginRequest",//訪問路徑
                    contentType: 'application/json;charset=utf-8',//返回json結果
                    data: JSON.stringify({"username":encryptUsername,"password":encryptPwd}),
                    headers: { "content-type": "application/json;charset=utf-8" },
                    success: function (data) {
                        console.log(data)

                    }
                });
            }
        });

    }
</script>

1.2 引入: jsencrypt.js 檔案,位置:/resources/static/js/ jsencrypt.js,附上開原始檔地址:獲取開源的 js 檔案:https://github.com/travist/jsencrypt/tree/master/bin

2.後端

2.1 RSA工具類


/**
 * 備註,解密前臺公鑰加密的資料,請呼叫decryptWithPrivate方法
 * 每次重啟之後,都會生成一個一對新的公私鑰
 */
public class RSAUtil {

    //祕鑰大小
    private static final int KEY_SIZE = 1024;

    //後續放到常量類中去
    public static final String PRIVATE_KEY = "privateKey";
    public static final String PUBLIC_KEY = "publicKey";

    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));
    }
}

2.2 獲取公鑰的RSAController類

@RestController
public class RSAController {

    @RequestMapping("/getPublicKey")
    public String getPublicKey(){
        return RSAUtil.getPublicKey();
    }
}

2.3 登入請求的處理介面:


@Controller
public class LoginController {

    //將Service注入Web層
    @Autowired
    UserService userService;

    
    /*讀取application.yml中的配置引數,在RSAUtil中我並沒有按照此方法去儲存PRIVATE_KEY和PUBLIC_KEY,建議將之寫至配置檔案中。*/
    @Value("${RSA.privateKey}")
    private String privateKey;

    @Value("${RSA.publicKey}")
    private String publicKey;

    @RequestMapping("/login")
    public String login() {
        return "login";
    }


    /**
     * 請求引數存放於request Payload中
     *
     * @param req
     * @return
     */
    @RequestMapping(value = "/loginRequest", method = RequestMethod.POST)
    @ResponseBody
    public String loginRequest(HttpServletRequest req) {
        //將request Payload中的引數轉化為JSONObject
        StringBuilder stringBuilder = new StringBuilder();
        try (BufferedReader reader = req.getReader();) {
            char[] buff = new char[1024];
            int jsonLength;
            while ((jsonLength = reader.read(buff)) != -1) {
                stringBuilder.append(buff, 0, jsonLength);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        JSONObject jsonObject = new JSONObject(stringBuilder.toString());
        //獲取JSONObject中的引數
        String username = jsonObject.getString("username");
        String password = jsonObject.getString("password");
        String passwordRSA = "";
        String usernameRSA = "";
        try {
            // 這裡就是解密後的密碼了
            usernameRSA = RSAUtil.decryptWithPrivate(username);
            passwordRSA = RSAUtil.decryptWithPrivate(password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //查下mysql中的使用者資訊
        return userService.loginRequest(usernameRSA, passwordRSA);
    }

}

2.4 通過Mapper訪問資料庫

public interface LoginService {

    String loginRequest(String userName, String password);
}
@Service
public class LoginServiceImpl implements LoginService {

    //將DAO注入Service層
    @Autowired
    private LoginMapper loginMapper;

    public String loginRequest(String userName, String password) {
        UserBean userBean = loginMapper.getInfo(userName, password);
        String loginMsg = "";
        if(userBean != null){
            loginMsg = "RSA加密和解密,登入成功!";
        }else{
            loginMsg = "登入失敗!";
        }
        return loginMsg;
    }
}

2.5 UserBean 為實體類:

注:其中的@Getter,@Setter和@Data都為lombok的註解,

@Getter
@Setter
@Data
public class UserBean {
    private String id;
    private String userName;
    private String password;
}

2.6 LoginMapper類

public interface LoginMapper {

    UserBean getInfo(String userName,String password);

}

2.7 LoginMapper.xml

注:其中的namespace根據真實專案中的檔案位置而定。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.test.mapper.LoginMapper">

    <select id="getInfo" parameterType="String" resultType="com.example.test.bean.LoginBean">
        SELECT * FROM t_user WHERE userName = #{userName} AND password = #{password}
    </select>

</mapper>

2.8 附上Application.yml檔案

server:
  port: 8080

spring:
  datasource:
    name: mysql
    url: jdbc:mysql://localhost:3306/mysql?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.test.bean

RSA:
  privateKey: privateKey
  publicKey: publicKey

三、測試,瀏覽器輸入:http://localhost:8080/login

3.1 測試獲取公鑰介面:

3.2 測試加密後的使用者名稱和密碼

使用者名稱和密碼在mysql中存在且正確,返回“RSA加密和解密,登入成功!”