账密登录方式中用户输入密码后,把账号、密码通过http传输到后端进行校验,然而密码属于敏感信息,不能以明文传输,否则容易被拦截窃取,因此需要考虑如何安全传输密码
使用rsa加密方式,rsa属于非对称加密,特点就是公钥加密私钥解密
生成公私钥,把公钥返回给前端,私钥用redis缓存
ManagerController.java
@GetMapping("/key")
public ResponseEntity<ApiResponse> key(@RequestParam("loginNo") String loginNo) {
String key = managerService.generateKey(loginNo);
return ApiResponse.success(key);
}
ManagerServiceImpl.java
@Override
public String generateKey(String loginNo) {
QueryWrapper<Manager> wrapper = new QueryWrapper<>();
wrapper.eq("loginNo", loginNo);
Manager entity = this.getOne(wrapper);
if (Objects.isNull(entity)) {
throw new CodeException("用户不存在:loginNo=" + loginNo);
}
try {
KeyPair keyPair = RSAUtil.generateKeyPair();
String publicKey = RSAUtil.getPublicKey(keyPair);
String privateKey = RSAUtil.getPrivateKey(keyPair);
log.info("publicKey={}", publicKey);
log.info("privateKey={}", privateKey);
String redisKey = RedisKey.MANAGE_LOGIN_RSA_PRIVATEKEY + "$" + loginNo;
// 清除缓存
redisService.del(redisKey);
// 私钥添加到缓存
redisService.set(redisKey, privateKey, 5, TimeUnit.MINUTES);
return publicKey;
} catch (Exception e) {
log.error("生成rsa密钥失败", e);
}
return null;
}
RSAUtil.java
public class RSAUtil {
private static final Charset CHARSET = StandardCharsets.UTF_8;
private static final String ALGORITHM = "RSA";
/**
* 生成密钥对
* @return
* @throws NoSuchAlgorithmException
*/
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGenerator.initialize(1024);
return keyPairGenerator.generateKeyPair();
}
/**
* 生成公钥
* @param keyPair
* @return
*/
public static String getPublicKey(KeyPair keyPair) {
PublicKey publicKey = keyPair.getPublic();
byte[] bytes = base64Encode(publicKey.getEncoded());
return new String(bytes, CHARSET);
}
/**
* 生成私钥
* @param keyPair
* @return
*/
public static String getPrivateKey(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
byte[] bytes = base64Encode(privateKey.getEncoded());
return new String(bytes, CHARSET);
}
/**
* base64加密
* @param bytes
* @return
*/
public static byte[] base64Encode(byte[] bytes) {
return Base64.getEncoder().encode(bytes);
}
/**
* base64解密
* @param bytes
* @return
*/
public static byte[] base64Decode(byte[] bytes) {
return Base64.getDecoder().decode(bytes);
}
/**
* base64解密
* @param src
* @return
*/
public static byte[] base64Decode(String src) {
return Base64.getDecoder().decode(src);
}
/**
* 解密
* @param key
* @param data
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
public static String decrypt(String key, String data) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
byte[] bytes = base64Decode(key);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(cipher.doFinal(base64Decode(data.getBytes(CHARSET))), CHARSET);
}
安装这个依赖
npm install [email protected]
import JSEncrypt from 'jsencrypt';
// key为后端返回的公钥,this.form.password为明文密码
const jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(key);
// pwd为加密后的密码
var pwd = jsEncrypt.encrypt(this.form.password);
从redis获取私钥,用私钥解密,得到明文后和数据库保存的密码比对,数据库的密码是以用户id为盐值对明文密码作md5加密,相同则放行,否则报错,注意登录成功的话需要把redis的密钥移除
ManagerController.java
@PostMapping("/login")
public ResponseEntity<ApiResponse> login(@RequestParam("loginNo") String loginNo, @RequestParam("password") String password)
throws Exception {
UserDTOWithToken dto = managerService.login(loginNo, password);
return ApiResponse.success(dto);
}
ManagerServiceImpl.java
@Override
public UserDTOWithToken login(String loginNo, String password) throws Exception {
QueryWrapper<Manager> wrapper = new QueryWrapper<>();
wrapper.eq("loginNo", loginNo);
Manager entity = this.getOne(wrapper);
if (Objects.isNull(entity)) {
throw new CodeException("用户不存在:loginNo=" + loginNo);
}
String redisKey = RedisKey.MANAGE_LOGIN_RSA_PRIVATEKEY + "$" + loginNo;
// 从缓存获取私钥
String privateKey = (String) redisService.get(redisKey);
if (StrUtil.isEmpty(privateKey)) {
throw new CodeException("私钥不存在");
}
// 用私钥解密
String realPassword = RSAUtil.decrypt(privateKey, password);
log.info("realPassword={}", realPassword);
// 校验密码
Digester digester = new Digester(DigestAlgorithm.MD5);
digester.setSalt(entity.getId().getBytes(StandardCharsets.UTF_8));
String encodePassword = digester.digestHex(realPassword);
log.info("encodePassword={}", encodePassword);
if (!encodePassword.equals(entity.getPassword())) {
throw new CodeException("密码错误");
}
//从认证服务获取token
ResponseEntity<ApiResponse> responseEntity = authClient.token(AuthConst.PASSWORD_GRANT_TYPE, AuthConst.ADMIN_CLIENT_ID,
AuthConst.ADMIN_CLIENT_SECRET, null, loginNo, password);
ApiResponse response = responseEntity.getBody();
TokenDTO tokenDTO = response.toObject(TokenDTO.class);
if (Objects.isNull(tokenDTO)) {
throw new CodeException("获取token失败:" + response.getMessage());
}
UserDTOWithToken dto = new UserDTOWithToken();
dto.setUserId(entity.getId());
dto.setToken(tokenDTO);
entity.setLoginTime(LocalDateTime.now());
Integer loginCount = entity.getLoginCount();
entity.setLoginCount(Objects.isNull(loginCount) ? 1 : loginCount + 1);
entity.setUpdateTime(LocalDateTime.now());
this.updateById(entity);
// 成功则清除缓存的私钥
redisService.del(redisKey);
return dto;
}
非对称加密还有其它算法,rsa是其中一种
后端存储私钥除了redis也可以用其它缓存工具如J2Cache