最近移植一个 flask 项目,里面使用了 werkzeug 进行加密。为了达到无缝转换,所以需要用 java 实现。
封面《ソーサレス*アライヴ!~the World’s End Fallen Star~》
前言
最近移植实验室的一个 flask
项目。其中使用了 werkzeug
中的 generate_password_hash
, check_password_hash
两个函数来加密密码和验证密码。为了两个后端之间的无缝衔接,需要使用两个后端加密验证结果一样,因此便有了本文。
网上解决方案
这种问题我认为还是比较常见的,先在网上搜了一下。在 csdn 上找到了如下代码。按照博主所说,此代码能够无缝衔接,然而在验证过程时发现加密结果不一样。
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 import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import java.math.BigInteger;import java.nio.charset.Charset;import java.security.NoSuchAlgorithmException;import java.security.spec.InvalidKeySpecException;import java.security.spec.KeySpec;import java.util.Random;public class Pbkdf2Sha256 { private static final Logger logger = LoggerFactory.getLogger(Pbkdf2Sha256.class); public static final int SALT_BYTE_SIZE = 16 ; public static final int HASH_BIT_SIZE = 64 * 4 ; private static final Integer DEFAULT_ITERATIONS = 2000 ; private static final String algorithm = "PBKDF2&SHA256" ; public static String getEncodedHash (String password, String salt, int iterations) { SecretKeyFactory keyFactory = null ; try { keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256" ); } catch (NoSuchAlgorithmException e) { logger.error("Could NOT retrieve PBKDF2WithHmacSHA256 algorithm" , e); } KeySpec keySpec = new PBEKeySpec (password.toCharArray(), salt.getBytes(Charset.forName("UTF-8" )), iterations, HASH_BIT_SIZE); SecretKey secret = null ; try { secret = keyFactory.generateSecret(keySpec); } catch (InvalidKeySpecException e) { logger.error("Could NOT generate secret key" , e); } return toHex(secret.getEncoded()); } private static byte [] fromHex(String hex) { byte [] binary = new byte [hex.length() / 2 ]; for (int i = 0 ; i < binary.length; i++) { binary[i] = (byte ) Integer.parseInt(hex.substring(2 * i, 2 * i + 2 ), 16 ); } return binary; } private static String toHex (byte [] array) { BigInteger bi = new BigInteger (1 , array); String hex = bi.toString(16 ); int paddingLength = (array.length * 2 ) - hex.length(); if (paddingLength > 0 ) return String.format("%0" + paddingLength + "d" , 0 ) + hex; else return hex; } public static String getsalt () { int length = SALT_BYTE_SIZE; Random rand = new Random (); char [] rs = new char [length]; for (int i = 0 ; i < length; i++) { int t = rand.nextInt(3 ); if (t == 0 ) { rs[i] = (char ) (rand.nextInt(10 ) + 48 ); } else if (t == 1 ) { rs[i] = (char ) (rand.nextInt(26 ) + 65 ); } else { rs[i] = (char ) (rand.nextInt(26 ) + 97 ); } } return new String (rs); } public static String encode (String password) { return encode(password, getsalt()); } public static String encode (String password, int iterations) { return encode(password, getsalt(), iterations); } public static String encode (String password, String salt) { return encode(password, salt, DEFAULT_ITERATIONS); } public static String encode (String password, String salt, int iterations) { String hash = getEncodedHash(password, salt, iterations); return String.format("%s$%d$%s$%s" , algorithm, iterations, salt, hash); } public static boolean verification (String password, String hashedPassword) { String[] parts = hashedPassword.split("\\$" ); if (parts.length != 4 ) { return false ; } Integer iterations = Integer.parseInt(parts[1 ]); String salt = parts[2 ]; String hash = encode(password, salt, iterations); return hash.equals(hashedPassword); } }
修改
这里先贴一段加密前后的代码和 python 的调用入口
1 2 3 4 5 # 加密前后的代码 # raw password 123456 # encoded password pbkdf2:sha256:260000$hxymrVhMaA4CszrW$460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450
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 def generate_password_hash ( password: str , method: str = "pbkdf2:sha256" , salt_length: int = 16 ) -> str : """Hash a password with the given method and salt with a string of the given length. The format of the string returned includes the method that was used so that :func:`check_password_hash` can check the hash. The format for the hashed string looks like this:: method$salt$hash This method can **not** generate unsalted passwords but it is possible to set param method='plain' in order to enforce plaintext passwords. If a salt is used, hmac is used internally to salt the password. If PBKDF2 is wanted it can be enabled by setting the method to ``pbkdf2:method:iterations`` where iterations is optional:: pbkdf2:sha256:80000$salt$hash pbkdf2:sha256$salt$hash :param password: the password to hash. :param method: the hash method to use (one that hashlib supports). Can optionally be in the format ``pbkdf2:method:iterations`` to enable PBKDF2. :param salt_length: the length of the salt in letters. """ salt = gen_salt(salt_length) if method != "plain" else "" h, actual_method = _hash_internal(method, salt, password) return f"{actual_method} ${salt} ${h} "
根据网上的代码和 python 的注释可以得知,加密后数据分为算法 pbkdf2:sha256
、迭代次数 260000
、盐 hxymrVhMaA4CszrW
和加密结果 460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450
四段。
因此对上述代码的以下部分进行修改,主要修改迭代次数,算法名,拼接过程和验证过程
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 private static final Integer DEFAULT_ITERATIONS = 260000 ;private static final String algorithm = "pbkdf2:sha256" ;public static String encode (String password, String salt, int iterations) { String hash = getEncodedHash(password, salt, iterations); return String.format("%s:%d$%s$%s" , algorithm, iterations, salt, hash); } public static boolean verification (String password, String hashedPassword) { String[] parts = hashedPassword.split("\\$" ); if (parts.length != 3 ) { return false ; } String[] parts2 = parts[0 ].split(":" ); if (parts2.length != 3 ) { return false ; } Integer iterations = Integer.parseInt(parts2[2 ]); String salt = parts[1 ]; String hash = encode(password, salt, iterations); return hash.equals(hashedPassword); }
完整代码
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 package com.zju.manager_svr.util;import java.math.BigInteger;import java.nio.charset.Charset;import java.security.NoSuchAlgorithmException;import java.security.spec.InvalidKeySpecException;import java.security.spec.KeySpec;import java.util.Random;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import lombok.extern.slf4j.Slf4j;@Slf4j public class HashUtil { public static final int SALT_BYTE_SIZE = 16 ; public static final int HASH_BIT_SIZE = 64 * 4 ; private static final Integer DEFAULT_ITERATIONS = 260000 ; private static final String algorithm = "pbkdf2:sha256" ; public static String getEncodedHash (String password, String salt, int iterations) { SecretKeyFactory keyFactory = null ; try { keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256" ); } catch (NoSuchAlgorithmException e) { log.error("Could NOT retrieve PBKDF2WithHmacSHA256 algorithm" , e); } KeySpec keySpec = new PBEKeySpec (password.toCharArray(), salt.getBytes(Charset.forName("UTF-8" )), iterations, HASH_BIT_SIZE); SecretKey secret = null ; try { secret = keyFactory.generateSecret(keySpec); } catch (InvalidKeySpecException e) { log.error("Could NOT generate secret key" , e); } return toHex(secret.getEncoded()); } private static byte [] fromHex(String hex) { byte [] binary = new byte [hex.length() / 2 ]; for (int i = 0 ; i < binary.length; i++) { binary[i] = (byte ) Integer.parseInt(hex.substring(2 * i, 2 * i + 2 ), 16 ); } return binary; } private static String toHex (byte [] array) { BigInteger bi = new BigInteger (1 , array); String hex = bi.toString(16 ); int paddingLength = (array.length * 2 ) - hex.length(); if (paddingLength > 0 ) return String.format("%0" + paddingLength + "d" , 0 ) + hex; else return hex; } public static String getsalt () { int length = SALT_BYTE_SIZE; Random rand = new Random (); char [] rs = new char [length]; for (int i = 0 ; i < length; i++) { int t = rand.nextInt(3 ); if (t == 0 ) { rs[i] = (char ) (rand.nextInt(10 ) + 48 ); } else if (t == 1 ) { rs[i] = (char ) (rand.nextInt(26 ) + 65 ); } else { rs[i] = (char ) (rand.nextInt(26 ) + 97 ); } } return new String (rs); } public static String encode (String password) { return encode(password, getsalt()); } public static String encode (String password, int iterations) { return encode(password, getsalt(), iterations); } public static String encode (String password, String salt) { return encode(password, salt, DEFAULT_ITERATIONS); } public static String encode (String password, String salt, int iterations) { String hash = getEncodedHash(password, salt, iterations); return String.format("%s:%d$%s$%s" , algorithm, iterations, salt, hash); } public static boolean verification (String password, String hashedPassword) { String[] parts = hashedPassword.split("\\$" ); if (parts.length != 3 ) { return false ; } String[] parts2 = parts[0 ].split(":" ); if (parts2.length != 3 ) { return false ; } Integer iterations = Integer.parseInt(parts2[2 ]); String salt = parts[1 ]; String hash = encode(password, salt, iterations); return hash.equals(hashedPassword); } }
验证
编写测试类对算法进行测试,验证结果是否一样。加密后的密码由 python 端提供,测试结果都通过说明加密结果一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.zju.manager_svr;import static org.junit.jupiter.api.Assertions.assertTrue;import com.zju.manager_svr.util.HashUtil;import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.CsvSource;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class HashUtilTest { @ParameterizedTest @CsvSource({ "123456,pbkdf2:sha256:260000$hxymrVhMaA4CszrW$460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450", "string,pbkdf2:sha256:260000$ygNNi7PGWBbb6QT1$82c85a39863313c75d6da0921f22d19ba501df387e9a7bdc30535ab4942109e9" }) public void passwordCheckTest (String password, String expected) { assertTrue(HashUtil.verification(password, expected)); } }
后记
本次移植过程中,密码加密一致大概是最麻烦的问题之一。在此解决这个问题以便日后翻阅。
参考
pbkdf2&sha256 加密验证算法 | 密码加密