后台管理系统的算术验证码登陆功能

后台管理系统的登陆功能: 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;
    }
}

总结

记录项目的一个登陆功能,还有一个发送验证码登陆功能,后续更新…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值