认证API:如何在客户端使用动态变更的Salt?
动态Salt下的客户端-服务端认证方案
核心思路
- 为每个用户分配唯一的随机Salt,由服务端在用户注册时生成,与哈希后的密码一同存储在数据库中
- 登录流程拆分为两步:客户端先请求目标用户的专属Salt,再用该Salt哈希密码后提交认证请求
- 全程无需硬编码固定Salt或密钥,所有动态生成的Salt由服务端统一管理并同步给客户端
数据库表结构调整
首先需要修改用户表,新增Salt字段存储每个用户的专属Salt:
ALTER TABLE customers.Unfallmeldung ADD COLUMN Salt VARCHAR(255) NOT NULL;
服务端代码修改(Node.js/Express)
新增获取用户Salt的接口,同时调整登录接口的验证逻辑:
const express = require('express'); const app = express(); const bodyParser = require('body-parser'); const mysql = require('mysql'); // 创建数据库连接池 const pool = mysql.createPool({ host: 'xx.xx.xx.xx', user: 'xxxx', password: 'XXXXXXXXX', database: 'customers' }); app.use(bodyParser.json()); // 获取用户专属Salt的接口 app.post('/get-salt', (req, res) => { const username = req.body.username; if (!username) { return res.status(400).json({message: 'Username is required'}); } pool.query('SELECT Salt FROM customers.Unfallmeldung WHERE Username = ?', [username], (error, results) => { if (error) { return res.status(500).json({error}); } if (results.length === 0) { return res.status(404).json({message: 'User not found'}); } res.status(200).json({salt: results[0].Salt}); }); }); // 修改后的登录验证接口 app.post('/login', (req, res) => { const username = req.body.username; const hashedPassword = req.body.password; if (!username || !hashedPassword) { return res.status(400).json({message: 'Username and password are required'}); } pool.query('SELECT * FROM customers.Unfallmeldung WHERE Username = ?', [username], (error, results) => { if (error) { return res.status(500).json({error}); } if (results.length === 0) { return res.status(401).json({message: 'Invalid credentials'}); } const storedHashedPassword = results[0].Password; // 直接对比客户端传来的哈希值与数据库存储的哈希值 if (hashedPassword === storedHashedPassword) { const response = [results[0].Company, results[0].Access, results[0].Databases]; return res.status(200).json({message: response}); } else { return res.status(401).json({message: 'Invalid credentials'}); } }); }); app.listen(3000, () => console.log('Server started on port 3000'));
客户端代码修改(Java)
调整登录逻辑,先获取用户Salt再哈希密码:
package com.pichlmayr.unfallmeldung; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; public class APIClient { private static final String API_URL_LOGIN = "https://xx.xx/login"; private static final String API_URL_GET_SALT = "https://xx.xx/get-salt"; public static String[][] authenticate(String username, String password) { try { // 第一步:获取用户专属Salt String salt = getSalt(username); if (salt == null) { System.out.println("Error: Failed to get salt for user"); return null; } // 第二步:用获取到的Salt哈希密码 String hashedPassword = HashPassword.encrypt(password, salt); // 第三步:提交登录请求 URL url = new URL(API_URL_LOGIN); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setDoOutput(true); String requestBody = "{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}"; OutputStream os = connection.getOutputStream(); os.write(requestBody.getBytes()); os.flush(); os.close(); // 处理响应 BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); String response = br.readLine(); br.close(); if (response.contains("Error: ") || response.contains("Invalid credentials")) { System.out.println("Error: " + response); return null; } // 解析响应(保留原逻辑) response = response.replace("\"", "") .replace("[", "") .replace("]", "") .replace("{", "") .replace("}", ""); response = response.substring(8); String[] responseArray = response.split(","); String[] company = new String[]{responseArray[0]}; String[] access = new String[]{responseArray[1]}; String[] databases = new String[responseArray.length - 2]; System.arraycopy(responseArray, 2, databases, 0, responseArray.length - 2); return new String[][]{company, access, databases}; } catch (Exception e) { e.printStackTrace(); } return null; } // 获取用户Salt的工具方法 private static String getSalt(String username) throws Exception { URL url = new URL(API_URL_GET_SALT); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setDoOutput(true); String requestBody = "{\"username\":\"" + username + "\"}"; OutputStream os = connection.getOutputStream(); os.write(requestBody.getBytes()); os.flush(); os.close(); BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); String response = br.readLine(); br.close(); if (response.contains("User not found") || response.contains("Error")) { return null; } // 解析返回的Salt值 return response.split("\"salt\":\"")[1].split("\"}")[0]; } }
哈希方法修改(Java)
调整为支持传入动态Salt,同时添加生成随机Salt的方法(用于用户注册):
package com.pichlmayr.unfallmeldung; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class HashPassword { // 接收外部传入的Salt进行哈希 public static String encrypt(String passwordToHash, String salt) throws NoSuchAlgorithmException { return get_SHA_512_SecurePassword(passwordToHash, salt); } private static String get_SHA_512_SecurePassword(String passwordToHash, String salt) { String generatedPassword = null; try { MessageDigest md = MessageDigest.getInstance("SHA-512"); md.update(salt.getBytes()); byte[] bytes = md.digest(passwordToHash.getBytes()); StringBuilder sb = new StringBuilder(); for (byte aByte : bytes) { sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1)); } generatedPassword = sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return generatedPassword; } // 生成随机Salt(用户注册时在服务端调用) public static String generateRandomSalt() throws NoSuchAlgorithmException { SecureRandom sr = SecureRandom.getInstanceStrong(); byte[] salt = new byte[16]; sr.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } }
额外说明
- 用户注册时,服务端需调用
generateRandomSalt()生成随机Salt,用该Salt哈希用户密码后,将用户名、Salt、哈希后的密码存入数据库 - 必须使用HTTPS传输所有请求,避免Salt和哈希值在网络中被窃听
- 若需进一步提升安全性,可采用挑战-响应模式:服务端生成随机挑战值,客户端用密码哈希挑战值后返回,服务器验证结果,防止重放攻击
内容的提问来源于stack exchange,提问作者Matze._
相关产品推荐
相关产品推荐

