app中使用短信登录,使用redis实现
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<artifactId>spring-data-redisartifactId>
<groupId>org.springframework.datagroupId>
exclusion>
<exclusion>
<artifactId>lettuce-coreartifactId>
<groupId>io.lettucegroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
<version>2.6.2version>
dependency>
<dependency>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
<version>6.1.6.RELEASEversion>
dependency>
redis:
host: 192.168.***.***
port: 6379
password: 123456
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
用户从前端发送手机号到后台服务器,首先需要经过service层验证手机号格式是否正确,将手机号传给某个接口(市面上很多),验证通过,发送验证码。然后service层随机生成一个6位数的随机字符串。之后首先将生成的验证码保存在redis中并设置有效时间(防止redis内存占满的情况),(这里采用String数据结构保存验证码,并且以手机号作为key)。然后向用户提供的手机号上发送本次随机生成的验证码。
@Override
public Result sendCode(String phone, HttpSession session) {
//1校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3符合生成验证码
String code = RandomUtil.randomNumbers(6);
//4保存验证码到redis,并设置有效期(防止内存占满setex)
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5发送验证码(本业务的功能在于学习redis,发送验证码需要整合第三方接口,这里假装发送一下)
log.debug("发送验证码成功,验证码为{}",code);
//返回
return Result.ok();
}
用户再次提交手机号和验证码,此时再次验证手机号格式是否正确,如果格式正确则去redis里查找该手机号对应的验证码,并与用户提供的验证码对比,如果两个验证码相同,则从数据库中查找到该user,并把用户信息也保存在redis中并设置有效时间以便后续功能的使用。(这里选择hash数据结构存储用户数据,因为考虑到用户的信息时一个个的对象)
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//用户提交过来手机号和验证码,对手机号和验证码进行校验
// 1.1校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//2如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
// 1.2校验验证码(从redis里获取)
String code = loginForm.getCode();//用户提交的验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
// 2不一致,报错
if(cacheCode == null || !cacheCode.toString().equals(code)){
return Result.fail("验证码错误");
}
// 3一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 4判断用户是否存在
// 5不存在,创建新用户
if (user == null) {
//创建新用户,并且保存
user = createUserWithPhone(phone);
}
// 6存在,保存用户信息到redis(采用hash作为存储数据结构,用一个随机生成的token作为key,不用手机号是因为最终的token要保存在浏览器,使用手机号的话不安全)
String token = UUID.randomUUID().toString(true);
// 6.1将用户对象转为hashmap存储
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((filedName,filedValue) -> filedValue.toString()));
//6.2存储到redis并设置有效时间(30min),并且当用户操作浏览器时更新有效期
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);
stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL,TimeUnit.MINUTES);
// 6.3通过登录拦截校验
return Result.ok(token);
}
由于登录后设置了用户保存在redis中的有效时间为30min,当30min过后,如果用户还在使用该app而信息过期,那么用户还需要再次登录,体验不好,应该是当用户在操作该app时去更新有效时间。保证用户使用时user信息永远不会过期。如何知道用户是否在操作app?可以在拦截器中对用户请求进行拦截,每当用户发送请求时,都会经过拦截器,所以可以在拦截器中更新有效时间
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
//这个类是我们自己手动写出来的,并没有添加到spring容器中,所以只能用构造放法的方式注入
public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1获取请求头中的token,判断是否登录
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
response.setStatus(401);
return false;
}
// 2基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// 3判断用户是否存在,主要判断登录是否过期
if (userMap.isEmpty()) {
// 4不存在,拦截,返回401
response.setStatus(401);
return false;
}
// 5将查询到的Hash数据转换为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns(
"/user/code",
"/user/login",
"/shop/**",
"/blog/hot",
"/upload/**",
"/shop-type/**",
"/voucher/**"
);
}
}
短信登录功能主要用到redis里的几种数据结构,其中String类型用于存储服务器生成的验证码,Hash用于存储用户的基本信息。这里存储的不需要是用户的完整信息,可以通过声明一个类,里面只有用户的最重要的信息就行。还需要设置这些键值对的过期时间,防止redis内存被占满的情况。还需要通过拦截器拦截用户的请求,以便更新用户信息的有效时间,防止用户在操作过程中信息过期。