从零学习一个基于Springboot的权限管理系统(二)验证码获取

487 阅读7分钟

验证码以及注销登录接口

一、生成验证码以及验证码校验

使用hutool的工具类 CodeGenerator生成图片验证码

  1. 配置文件增加Captcha相关配置
  • 包括验证码类型(circle-圆圈干扰验证码 gif-GIT验证码 line-干扰线验证码 shear-扭曲干扰验证码)、
  • 验证码宽度
  • 验证码高度
  • 验证码元素干扰个数
  • 文本透明度(0.0-1.0)
  • 验证码字符配置 类型(math、random) 验证码长度
  • 字体名称 字体大小 验证码有效期
# 验证码配置
captcha:
  # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
  type: circle
  # 验证码宽度
  width: 120
  # 验证码高度
  height: 40
  # 验证码干扰元素个数
  interfere-count: 2
  # 文本透明度(0.0-1.0)
  text-alpha: 0.8
  # 验证码字符配置
  code:
    # 验证码字符类型 math-算术|random-随机字符
    type: math
    # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数
    length: 1
  # 验证码字体
  font:
    # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif
    name: SansSerif
    # 字体样式 0-普通|1-粗体|2-斜体
    weight: 1
    # 字体大小
    size: 24
  # 验证码有效期(秒)
  expire-seconds: 120
  1. 使用property类对自定义验证码属性进行读取,声明一个CaptchaProperties类并添加@ConfigurationProperties注解。属性同名
  2. 添加验证码自动装配配置类,根据配置文件验证码类型创建不同的验证码生成对象
@Configuration
public class CaptchaConfig {

    @Autowired
    private CaptchaProperties captchaProperties;

    /**
     * 验证码文字生成器
     *
     * @return CodeGenerator
     */
    @Bean
    public CodeGenerator codeGenerator() {
        String codeType = captchaProperties.getCode().getType();
        int codeLength = captchaProperties.getCode().getLength();
        if ("math".equalsIgnoreCase(codeType)) {
            return new MathGenerator(codeLength);
        } else if ("random".equalsIgnoreCase(codeType)) {
            return new RandomGenerator(codeLength);
        } else {
            throw new IllegalArgumentException("Invalid captcha generator type: " + codeType);
        }
    }

    /**
     * 验证码字体
     */
    @Bean
    public Font captchaFont() {
        String fontName = captchaProperties.getFont().getName();
        int fontSize = captchaProperties.getFont().getSize();
        int fontWight = captchaProperties.getFont().getWeight();
        return new Font(fontName, fontWight, fontSize);
    }


}
  1. 编写业务逻辑 AuthController--->AuthService--->AuthServiceImpl 将CodeGenerator注入到AuthServiceImpl中,根据配置生产出不同的验证码,将生成的CaptchaCode以及对应的Base64编码存储下来。生成验证码key,用以存储到redis中。将key以及base64编码图片发送给前端
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);

String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();

// 验证码文本缓存至Redis,用于登录校验
String captchaKey = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,
       captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);

return CaptchaResult.builder()
       .captchaKey(captchaKey)
       .captchaBase64(imageBase64Data)
       .build();
  1. 验证码校验时,根据用户登录时提交上来的captchakey从redis中获取对应的captchacode。与前端提交上来的captchacode进行比对。相等放行,不相等直接给前端返回响应的错误码。

二、Hutool常用的工具类介绍 (官网链接为: doc.hutool.cn/)

类型转换工具类 Convert

基本类型转换

int a = 1;
//aStr为"1"
String aStr = Convert.toStr(a);

long[] b = {1,2,3,4,5};
//bStr为:"[1, 2, 3, 4, 5]"
String bStr = Convert.toStr(b);

通过convert(TypeReference<T> reference, Object value)方法,自行new一个TypeReference对象可以对嵌套泛型进行类型转换。例如,我们想转换一个对象为List<String>类型,此时传入的标准Class就无法满足要求,此时我们可以这样:

Object[] a = { "a", "你", "好", "", 1 };
List<String> list = Convert.convert(new TypeReference<List<String>>() {}, a);

还包括半角全角转换、16进制、编码转换、金额大小写、时间单位换算等等

日期时间转换类DateUtil以及LocalDateUtil

LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

// "2020-01-23 12:23:56"
String format = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATETIME_PATTERN);

3. 字符串工具类 StrUtil

这个工具的用处类似于Apache Commons Lang (opens new window)中的StringUtil,之所以使用StrUtil而不是使用StringUtil是因为前者更短,而且Str这个简写我想已经深入人心了,大家都知道是字符串的意思。常用的方法例如isBlankisNotBlankisEmptyisNotEmpty

4. 对象工具类 ObjectUtil

ObjectUtil.equal

比较两个对象是否相等,相等需满足以下条件之一:

  1. obj1 == null && obj2 == null
  2. obj1.equals(obj2)
Object a = null;
Object b = null;

// true
ObjectUtil.equals(a, b);

ObjectUtil.contains

对象中是否包含元素。

支持的对象类型包括:

  • String
  • Collection
  • Map
  • Iterator
  • Enumeration
  • Array
int[] array = new int[]{1,2,3,4,5};

// true
final boolean contains = ObjectUtil.contains(array, 1);

还有常用 isNull 和 isNotNull判断

唯一id工具 IdUtil

  • UUID
  • ObjectId(MongoDB)
  • Snowflake(Twitter)

信息脱敏工具 DesensitizedUtil

在数据处理或清洗中,可能涉及到很多隐私信息的脱敏工作,因此Hutool针对常用的信息封装了一些脱敏方法。

支持的脱敏数据类型包括:

  1. 用户id
  2. 中文姓名
  3. 身份证号
  4. 座机号
  5. 手机号
  6. 地址
  7. 电子邮件
  8. 密码
  9. 中国大陆车牌,包含普通车辆、新能源车辆
  10. 银行卡

整体来说,所谓脱敏就是隐藏掉信息中的一部分关键信息,用*代替,自定义隐藏可以使用StrUtil.hide方法完成 我们以身份证号码为例:

// 5***************1X
DesensitizedUtil.idCardNum("51343620000320711X", 1, 2);

对于约定俗成的脱敏,我们可以不用指定隐藏位数,比如手机号:

// 180****1999
DesensitizedUtil.mobilePhone("18049531999");

当然还有一些简单粗暴的脱敏,比如密码,只保留了位数信息:

// **********
DesensitizedUtil.password("1234567890");

断言 Assert类

主要作用是在方法或者任何地方对参数的有效性做校验。当不满足断言条件时,会抛出IllegalArgumentExceptionIllegalStateException异常。

  • isTrue 必须为true,否则抛出IllegalArgumentException异常
  • isNull 必须是null值
  • notNull 不能是null值
  • notEmpty 不能为空,支持字符串,数组,集合等
  • notBlank 不能是空白字符串
  • notContain 不能包含指定的子串
  • noNullElements 数组中不能包含null元素
  • isInstanceOf 必须是指定类的实例
  • isAssignable 必须是子类和父类关系
  • state 会抛出IllegalStateException异常

JsonUtil工具类

JSON字符串创建

JSONUtil.toJsonStr可以将任意对象(Bean、Map、集合等)直接转换为JSON字符串。 如果对象是有序的Map等对象,则转换后的JSON字符串也是有序的。

SortedMap<Object, Object> sortedMap = new TreeMap<Object, Object>() {
	private static final long serialVersionUID = 1L;
	{
	put("attributes", "a");
	put("b", "b");
	put("c", "c");
}};

//{"attributes":"a","b":"b","c":"c"}
JSONUtil.toJsonStr(sortedMap);

JSON字符串解析

String html = "{"name":"Something must have been changed since you leave"}";
JSONObject jsonObject = JSONUtil.parseObj(html);
jsonObject.getStr("name");

JSON转Bean

String json = "{"ADT":[[{"BookingCode":["N","N"]}]]}";

Price price = JSONUtil.toBean(json, Price.class);

JWTUtil工具类

  • JWT创建
Map<String, Object> map = new HashMap<String, Object>() {
	private static final long serialVersionUID = 1L;
	{
		put("uid", Integer.parseInt("123"));
		put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
	}
};

JWTUtil.createToken(map, "1234".getBytes());
  • JWT解析
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
	"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
	"U2aQkC2THYV9L0fTN-yBBI7gmo5xhmvMhATtu8v0zEA";

final JWT jwt = JWTUtil.parseToken(rightToken);

jwt.getHeader(JWTHeader.TYPE);
jwt.getPayload("sub");
  • JWT验证
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
	"eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MjQwMDQ4MjIsInVzZXJJZCI6MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV_op5LoibLkuozlj7ciLCJzeXNfbWVudV8xIiwiUk9MRV_op5LoibLkuIDlj7ciLCJzeXNfbWVudV8yIl0sImp0aSI6ImQ0YzVlYjgwLTA5ZTctNGU0ZC1hZTg3LTVkNGI5M2FhNmFiNiIsImNsaWVudF9pZCI6ImhhbmR5LXNob3AifQ." +
	"aixF1eKlAKS_k3ynFnStE7-IRGiD5YaqznvK2xEjBew";

JWTUtil.verify(token, "123456".getBytes());

三、 用户注销

业务逻辑比较简单,重点是验证token是否过期。如果没过期,需要将该token对应的jwt_id 存储到redis中设置为黑名单。不能再通过该token实现数据鉴权

/**
 * 注销
 */
@Override
public void logout() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String token = request.getHeader(HttpHeaders.AUTHORIZATION);
    if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
        token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
        // 解析Token以获取有效载荷(payload)
        JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
        // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间)
        String jti = payloads.getStr(JWTPayload.JWT_ID);
        Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒)
        // 如果exp存在,则计算Token剩余有效时间
        if (expiration != null) {
            long currentTimeSeconds = System.currentTimeMillis() / 1000;
            if (expiration < currentTimeSeconds) {
                // Token已过期,不再加入黑名单
                return;
            }
            // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除
            long ttl = expiration - currentTimeSeconds;
            redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS);
        } else {
            // 如果exp不存在,说明Token永不过期,则永久加入黑名单
            redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null);
        }
    }
    // 清空Spring Security上下文
    SecurityContextHolder.clearContext();
}