Redis企业实战—基于Redis短信验证功能
文章目录
- Redis企业实战---基于Redis短信验证功能
- 一、短信登录实现
- 1.1、导入[黑马点评项目](https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwd=eh11)
- 1.2、基于Session实现
- 1.3、集群的session的共享问题
- 1.4、基于Redis实现共享session登录
Redis可以进行如下操作
一、短信登录实现
1.1、导入黑马点评项目
表的基本信息
前后端分离
导入后端项目
导入前端项目
运行前端项目
1.2、基于Session实现
实现流程
发送验证码
发送验证码
主要进行如下操作:
- 对手机号进行校验
- 模拟生成一个验证码并保存在session中
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone))
{
// 2.不符合就返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 符合,生成验证码--一般长度为6位 用一个随机数默认向手机号发送短信
String code = RandomUtil.randomNumbers(6);
// 4.将验证码保存到session中
session.setAttribute("code",code);
// 5.发送验证码--aliyun的第三方库
log.debug("验证码发送成功,验证码:"+code);
// 5.返回ok
return Result.ok();
}
登录
主要进行如下操作:
- 通过数据绑定获取到前端的表单数据
- 对手机号进行校验
- 对验证码进行检验看是否和存在session中的验证码一致
- 按手机号对user表进行查询
- 没有当前用户,就创建为新用户并保存到session中
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 不符合就返回错误信息
return Result.fail("手机号格式错误!");
}
// 2.继手机号之后,校验验证码
String code = loginForm.getCode();
String cacheCode = (String) session.getAttribute("code");
if (cacheCode == null || RegexUtils.isCodeInvalid(code) || cacheCode == code) {
// 3.不一致,报错
return Result.fail("验证码错误!");
}
// 4.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null){
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
session.setAttribute("user",user);
// 每一个session都有一个SessionID
return Result.ok();
}
private User createUserWithPhone(String phone) {
// 1. 创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}
登录验证功能
定义拦截器
主要进行如下操作:
- 获取session中的用户
- 判断用户是否存在,
- 存在,保存用户信息到 ThreadLocal
- 放行
public class LoginInterceptor implements HandlerInterceptor {
// 1. 前置拦截器
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取session
HttpSession session = request.getSession();
// 2. 获取session中的用户
Object user = session.getAttribute("user");
// 3. 判断用户是否存在
if (user == null){
// 4. 不存在,拦截,返回401代码
response.setStatus(401);
return false;
}
// 5. 存在,保存用户信息到 ThreadLocal
UserHolder.saveUser((User) user);
// 6. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
配置拦截器
主要进行如下操作:
- 将定义的LoginInterceptor登录拦截器添加进去
- 并排除一些不需要拦截的请求
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 将定义的LoginInterceptor登录拦截器添加进去
// 并排除一些不需要拦截的请求
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/voucher/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
1.3、集群的session的共享问题
session实现共享所面临的问题:
Redis代替session需要考虑的问题
- 选择合适的数据结构
- 选择合适的key
- 选择合适粒度
短信验证和登录注册
验证码发送
主要进行如下操作:
- 检验手机号是否合法
- 生成一个模拟验证码,并将验证码保存到redis中
- 给保证验证码的key设置有效期
@Autowired
private StringRedisTemplate stringRedisTemplate;
//引入ObjectMapperJSON处理类
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone))
{
// 2.不符合就返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 符合,生成验证码--一般长度为6位 用一个随机数默认向手机号发送短信
String code = RandomUtil.randomNumbers(6);
// 4.将验证码保存到Redis中 --- 一般加个前缀即能保证唯一性,又能指明当前key-value的功能
// 5.设置验证码的key的有效期 LOGIN_CODE_KEY:前缀 LOGIN_CODE_TTL:有效期时间
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 6.发送验证码--aliyun的第三方库
log.debug("验证码发送成功,验证码:"+code);
// 7.返回ok
return Result.ok();
}
校验登录状态
登录凭证token的存储方式
登录代码
主要操作分为以下几点:
- 获取表单的数据,进行手机号的验证
- 从redis中读取保存的验证码(前缀+手机号为key),并判断是否正确
- 当数据符合要求时,利用手机号进行查询user表
- 当没有数据的时候,进行创建新用户并保存
- 保存用户信息到redis
- 用随机token 作为登录令牌
- 将Java对象转换为Hash进行存储
- 设置token有效期
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 不符合就返回错误信息
return Result.fail("手机号格式错误!");
}
// TODO 2.从redis获取到验证码并进行验证
String code = loginForm.getCode();
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
if (cacheCode == null || RegexUtils.isCodeInvalid(code) || cacheCode == code) {
// 3.不一致,报错
return Result.fail("验证码错误!");
}
// 4.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null){
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// TODO 7. 保存用户信息到redis,问题进行保存的只能是用户的部分信息,不能包括完整信息,用户的敏感数据必须被隐藏。UserDTD类只包含数据的部分信息
// 7.1、用随机token 作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2、将Java对象转换为Hash进行存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> map = BeanUtil.beanToMap(userDTO);
String key = LOGIN_USER_KEY+token;
map.put("id",userDTO.getId().toString());
stringRedisTemplate.opsForHash().putAll(key,map);
// 7.3、设置token有效期 30分钟 CACHE_SHOP_TTL常量
stringRedisTemplate.expire(key,CACHE_SHOP_TTL,TimeUnit.MINUTES);
return Result.ok(token);
}
拦截器配置
主要获得请求头携带的token(登录动作的返回值,在前端作为数据存储在请求头中),方便后续的操作。主要操作:
- 创建构造函数,方便MvcConfig 注入StringRedisTemplate对象
- 获取请求头中的token
- 基于token获取reids中的用户
- 判断用户是否为空
- 将查询到的hash数据转为UserDTD对象
- 存在,保存用户信息到 ThreadLocal
- 刷新token的有效期
- 放行
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
// 1. 前置拦截器
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
// 不存在,拦截,返回401代码
response.setStatus(401);
return false;
}
// 2. 基于token获取reids中的用户
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
// 3. 判断是否是空值
if (map.isEmpty()){
// 4. 不存在,拦截,返回401代码
response.setStatus(401);
return false;
}
// 5. 将查询到的hash数据转为UserDTD对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
// 6. 存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7. 刷新token的有效期
String key = LOGIN_USER_KEY + token;
stringRedisTemplate.expire(key,30, TimeUnit.MINUTES);
// 8. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
注入拦截器
将定义的LoginInterceptor登录拦截器注入到spring中,需要进行以下操作:
- 排序一些不需要拦截的请求
- 注入StringRedisTemplate 对象
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 排除一些不需要拦截的请求
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/shop/**",
"/shop-type/**",
"/voucher/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}