后台管理系统的登陆功能: SpringSecurity + AES(加密解密算法) + Java算术验证码 + SpringDateRedis的RedisTemplate
前言
单纯的记录一下我们项目里后台管理系统的登陆功能 包含 验证码 登陆校验 AES解密。
一、登陆的大概逻辑梳理一下
1.登录前用算术验证码工具生成验证码,将验证码计算结果放入缓存,最终返回前端生成的base64验证码和存入缓存的key用于校验用户登陆;
2.用户开始登陆,前端传入1 返回的随机的缓存用来获取验证码,校验验证码, 校验账号,解密加密的密码,使用 org.springframework.security.crypto.password 校验解密后的密码,校验登陆用户禁用状态,
3.设置用户登陆信息,设置用户系统菜单权限,设置用户登陆token,
二、使用步骤
1.引入库
代码如下(示例):
<!-- 安全 spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 开始spring 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--缓存 spring data redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
<!-- 校验 validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.登录前调用生成算术验证码接口
代码如下(示例):
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping(value = "/get")
@ApiOperation(value = "获取登录验证码图片")
public ResultVO<Object> getCode() {
//设置验证码图形的宽和高
ArithmeticCaptcha captcha = new ArithmeticCaptcha(111, 36);
// 获取运算的公式:4-9+1=?
captcha.getArithmeticString();
// 获取运算的结果:-4
String value = captcha.text();
//随机一个字符串作为缓存运算结果的key 也把这个key返给了前端,在登录接口里用到
String redisKey = IdUtil.simpleUUID();
//生成的验证码事先加入缓存 缓存5分钟 给用户足够的时间进行简单计算
redisTemplate.opsForValue().set(redisKey, value, 300L, TimeUnit.SECONDS);
//将算术验证码转成base64码返给前端显示,同时把随机的缓存key返给前端,前端再在登录接口里传入用于获取这里生成的验证码运算结果
Map<Object, Object> objectMap = MapUtil.builder().put("img", captcha.toBase64()).put("uuid", redisKey).build();
//返回
return ResponseUtil.success(objectMap);
}
3.前端调用登录接口传参(前端要把验证码接口里随机出来的缓存key传过来)
//子类继承父类
@Data
public class CaptchaAuthenticationDTO extends AuthenticationDTO {
@ApiModelProperty(value = "登陆页面上的验证码计算的结果", required = true)
private String code;
@ApiModelProperty(value = "生成验证码时随机缓存的key 这个key对应着提前运算好的结果", required = true)
private String uuid;
}
//父类里定义 登录的 账号和密码
@Data
public class AuthenticationDTO {
/**
* 用户名
*/
@NotBlank(message = "userName不能为空")
@ApiModelProperty(value = "用户名/邮箱/手机号", required = true)
protected String userName;
/**
* 密码
*/
@NotBlank(message = "passWord不能为空")
@ApiModelProperty(value = "一般用作密码", required = true)
protected String passWord;
}
4.登录接口
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
@ApiOperation(value = "账号密码 + 验证码登录(用于后台登录)", notes = "通过用户名+密码+验证码登录 前端传入生成验证码接口返回的随机的缓存用来获取验证码")
public ResponseEntity<?> login(@Valid @RequestBody CaptchaAuthenticationDTO captchaAuthenticationDTO) {
// 获取验证码生成接口存的验证码运算结果
String redisCode = redisTemplate.opsForValue().get(captchaAuthenticationDTO.getUuid());
//校验验证码
if(redisCode == null || !redisCode.equals(captchaAuthenticationDTO.getCode())){
throw new AdminGlobalException(0,"验证码运算结果不正确");
}
//校验账号
SysUser sysUser = sysUserService.getByUserName(captchaAuthenticationDTO.getUserName());
if (sysUser == null) {
throw new AdminGlobalException(0,"账号不正确");
}
//AES解密加密的密码:
String decryptPassword = passwordManager.decryptPassword(captchaAuthenticationDTO.getPassWord());
//使用 org.springframework.security.crypto.password包下的类 校验解密后的密码
if (StrUtil.isBlank(sysUser.getPassword()) || !passwordEncoder.matches(decryptPassword ,sysUser.getPassword())){
throw new AdminGlobalException(0,"密码不正确");
}
// 校验登陆用户禁用状态 不是店铺超级管理员,并且是禁用状态,无法登录
if (Objects.equals(sysUser.getStatus(),0)) {
// 未找到此用户信息
throw new AdminGlobalException(0,"未找到此用户信息");
}
//设置用户登陆信息,
UserInfoInTokenBO userInfoInToken = new UserInfoInTokenBO();
userInfoInToken.setUserId(String.valueOf(sysUser.getUserId()));
userInfoInToken.setSysType(SysTypeEnum.ADMIN.value());
userInfoInToken.setEnabled(sysUser.getStatus() == 1);
userInfoInToken.setNickName(sysUser.getNickName());
userInfoInToken.setUserName(sysUser.getUsername());
userInfoInToken.setShopId(sysUser.getShopId());
//设置用户系统菜单权限
userInfoInToken.setPerms(sysUserService.getUserPermissions(sysUser.getUserId()));
// 设置用户登陆token
TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken);
return ResponseEntity.ok(tokenInfoVO);
}
4.登陆token获取设置; 反复阅读代码整理了storeAndGetVo(UserInfoInTokenBO )方法的逻辑
UserInfoInTokenBO userInfoInToken = new UserInfoInTokenBO();
设置用户登陆信息 设置登陆用户拥有的菜单权限 设置登陆用户的用户名 用户昵称 用户所在租户 用户拥有的系统菜单权限
先把 UserInfoInTokenBO 对象 传给 TokenStore 对象里的一个方法() 最终返回一个 TokenInfoVO 对象出来
TokenInfoVO tokenInfoVO = new TokenInfoVO();
TokenStore 对象里的这个方法里 又调用了一个方法 storeAccessToken() ,这个方法 同样传入 UserInfoInTokenBO 对象 返回一个 TokenInfoBO 对象
TokenInfoBO tokenInfoBO = new TokenInfoBO();
private UserInfoInTokenBO userInfoInToken;
private String accessToken;
private String refreshToken;
private Integer expiresIn;
1. 把整个 UserInfoInTokenBO 对象设置到 TokenInfoBO 对象中作为一个属性
private UserInfoInTokenBO userInfoInToken;
2.根据 UserInfoInTokenBO 对象的一个属性 系统类型 来设置 TokenInfoBO 对象的过期时间属性 private Integer expiresIn;
3. 从UserInfoInTokenBO 对象的属性 系统类型 和 userId这两个属性 获取私有key key的取值是 “sysType:userId” spel三目运算 结果 系统类型 / 系统类型 : 系统的用户id
拼接一个key 老长了拐弯抹角的拼接字符串烦死了 最后结果拼成一个key ==> String key = "ydgf_admin:ydgf_oauth:token:uid_to_access:sysType:userId "
这个拼接的key 用在了
存:
byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
connection.sAdd(uidKey, ArrayUtil.toArray(existsAccessTokensBytes, byte[].class));
connection.expire(uidKey, expiresIn);
取:
Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size);
4.随机一个字符串 String accessToken = IdUtil.simpleUUID(); 用这个随机的字符串拼接一个key accessKey--> == "ydgf_admin:ydgf_oauth:token:access:随机str
这个key 用在了
存:
byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);
connection.setEx(accessKey, expiresIn, JSON.toJSON(userInfoInToken).toString().getBytes());
5.再随机一个字符串 String refreshToken = IdUtil.simpleUUID(); 用这个随机字符串拼接一个key refreshAccessKey--> == "ydgf_admin:ydgf_oauth:token:refresh_to_access:随机str
这个key用在了
存:
byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));
6. 将随机出来的字符串 都加密处理 分别设置到 TokenInfoBO 的属性中
private String accessToken;
private String refreshToken;
7.返回设置好的 TokenInfoBO 对象
8.原封不动的把 TokenInfoBO 对象的属性值 设置到 TokenInfoVO 对象中
@ApiModelProperty("accessToken")
private String accessToken;
@ApiModelProperty("refreshToken")
private String refreshToken;
@ApiModelProperty("在多少秒后过期")
private Integer expiresIn;
5.登陆密码解密
import cn.hutool.crypto.symmetric.AES;
import com.shop.exception.AdminGlobalException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
@Component
public class PasswordManager {
private static final Logger logger = LoggerFactory.getLogger(PasswordManager.class);
/**
* 用于aes签名的key,16位
*/
@Value("${auth.password.signKey:-yydyyd-password}")
public String passwordSignKey;
//给…解密
public String decryptPassword(String data) {
//AES 解密
AES aes = new AES(passwordSignKey.getBytes(StandardCharsets.UTF_8));
String decryptStr;
String decryptPassword;
try {
// 解密
decryptStr = aes.decryptStr(data);
decryptPassword = decryptStr.substring(13);
} catch (Exception e) {
logger.error("Exception:", e.getMessage());
throw new AdminGlobalException(0,"AES解密错误");
}
return decryptPassword;
}
}
总结
记录项目的一个登陆功能,还有一个发送验证码登陆功能,后续更新…