上一篇《springboot项目使用redis、springSecurity、jwt实现单点登录》写了关于单点登录的架子,但是没有实现密码验证的细节。这里使用盐和摘要算法来实现一个密码验证的完整过程demo。
1、依赖没变,还是上一篇内容那些
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
2、POJO类,新增了PO用于注册用户的持久化
public class UserDO {
private String name;
private String password;
public UserDO() {
}
public UserDO(String name, String password) {
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class UserPO {
private String username;
private String password;
private String salt;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
}
3、service层
package com.loong.nba.player.service;
import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;
public interface UserService {
/**
* 添加用户
* @return 用户id
*/
UserPO addUser(UserDO userDO);
boolean comparePassword(UserDO userDO);
}
package com.loong.nba.player.service;
import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* @author
* @date 2023/5/19
*/
@Service
public class UserServiceImpl implements UserService {
private final RedisTemplate<String, UserPO> redisTemplate;
public UserServiceImpl(RedisTemplate<String, UserPO> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public UserPO addUser(UserDO userDO) {
// 定义盐的长度(字节数)
int saltLength = 6;
// 创建一个安全的随机数生成器
SecureRandom secureRandom = new SecureRandom();
// 生成盐
byte[] salt = new byte[saltLength];
secureRandom.nextBytes(salt);
// 将盐转换为字符串或字节数组
String saltString = encodeSalt(salt);
// 计算密码摘要
String password = encryptPassword(userDO.getPassword(), saltString);
UserPO userPO = new UserPO();
userPO.setUsername(userDO.getName());
userPO.setPassword(password);
userPO.setSalt(saltString);
redisTemplate.opsForValue().set(userPO.getUsername()+"账号", userPO);
// byte[] saltBytes = decodeSaltString(saltString);
System.out.println("Salt (Base64 string): " + saltString);
// System.out.println("Salt (byte array): " + Base64.getEncoder().encodeToString(saltBytes));
return userPO;
}
String encodeSalt(byte[] salt) {
return Base64.getEncoder().encodeToString(salt);
}
byte[] decodeSaltString(String saltString) {
return Base64.getDecoder().decode(saltString);
}
String encryptPassword(String password, String salt) {
String plaintext = password + salt;
try {
// 创建SHA-256算法的MessageDigest实例
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 计算中文字符串的摘要
byte[] hash = digest.digest(plaintext.getBytes(StandardCharsets.UTF_8));
// 将摘要字节数组转换为十六进制字符串表示
String digestHex = bytesToHex(hash);
System.out.println("SHA-256摘要:" + digestHex);
return digestHex;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
// 将摘要字节数组转换为十六进制字符串表示
private static String bytesToHex(byte[] bytes) {
StringBuilder hexStringBuilder = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexStringBuilder.append('0');
}
hexStringBuilder.append(hex);
}
return hexStringBuilder.toString();
}
@Override
public boolean comparePassword(UserDO userDO) {
UserPO user = redisTemplate.opsForValue().get(userDO.getName()+"账号");
String salt = user.getSalt();
String password = encryptPassword(userDO.getPassword(), salt);
if (user.getPassword().equals(password)) {
return true;
}
return false;
}
}
4、配置类
redis配置我用的spring原生的写法
package com.loong.nba.player.config;
import com.loong.nba.player.pojo.UserPO;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author
* @date 2023/5/22
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, UserPO> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, UserPO> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//设置key序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
//设置value的序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
这个security配置中区别是将新增的注册接口“/user”加入到允许放行的白名单中
5、controller
package com.loong.nba.player.controller;
import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.pojo.UserPO;
import com.loong.nba.player.service.UserServiceImpl;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
/**
* @author
* @date 2023/5/15
*/
@RestController
public class LoginController {
private final JwtUtil jwtUtil;
private final UserServiceImpl userService;
public LoginController(JwtUtil jwtUtil, UserServiceImpl userService) {
this.jwtUtil = jwtUtil;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<UserDO> postLogin(@RequestBody UserDO userDO, HttpServletResponse response) {
if (!userService.comparePassword(userDO)) {
return ResponseEntity.ok(new UserDO());
}
String token = jwtUtil.generateToken(userDO.getName());
response.addHeader("Authorization", "Bearer " + token);
return ResponseEntity.ok(userDO);
}
@PostMapping("/user")
public UserPO addUser(@RequestBody UserDO userDO) {
return userService.addUser(userDO);
}
@GetMapping("/test")
public String test() {
return "hello test";
}
}
6、演示步骤
可以用任何接口测试工具,我用的是postman
第一步通过/user接口新增一个用户;
第二步通过/login接口登录用户,获取返回的Headers中的token;
第三步用这个token,加到请求头中,请求test接口,就形成了完整的闭环。