验证码以及注销登录接口
一、生成验证码以及验证码校验
使用hutool的工具类 CodeGenerator生成图片验证码
配置文件增加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
- 使用property类对自定义验证码
属性进行读取,声明一个CaptchaProperties类并添加@ConfigurationProperties注解。属性同名 - 添加验证码自动装配配置类,根据配置文件验证码类型创建不同的
验证码生成对象
@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);
}
}
- 编写业务逻辑 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();
- 验证码校验时,根据用户登录时提交上来的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这个简写我想已经深入人心了,大家都知道是字符串的意思。常用的方法例如isBlank、isNotBlank、isEmpty、isNotEmpty
4. 对象工具类 ObjectUtil
ObjectUtil.equal
比较两个对象是否相等,相等需满足以下条件之一:
- obj1 == null && obj2 == null
- 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针对常用的信息封装了一些脱敏方法。
支持的脱敏数据类型包括:
- 用户id
- 中文姓名
- 身份证号
- 座机号
- 手机号
- 地址
- 电子邮件
- 密码
- 中国大陆车牌,包含普通车辆、新能源车辆
- 银行卡
整体来说,所谓脱敏就是隐藏掉信息中的一部分关键信息,用*代替,自定义隐藏可以使用StrUtil.hide方法完成
我们以身份证号码为例:
// 5***************1X
DesensitizedUtil.idCardNum("51343620000320711X", 1, 2);
对于约定俗成的脱敏,我们可以不用指定隐藏位数,比如手机号:
// 180****1999
DesensitizedUtil.mobilePhone("18049531999");
当然还有一些简单粗暴的脱敏,比如密码,只保留了位数信息:
// **********
DesensitizedUtil.password("1234567890");
断言 Assert类
主要作用是在方法或者任何地方对参数的有效性做校验。当不满足断言条件时,会抛出IllegalArgumentException或IllegalStateException异常。
- 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();
}