目录
- 短信登录功能的实现
- 一:基于session进行短信登录
- 1:发送验证码
- 2:登录
- 3:登录验证拦截器
- 4:隐藏用户敏感信息
- 二:session的集群共享问题
- 三:基于redis实现短信登录
- 登录的刷新问题
短信登录功能的实现
一:基于session进行短信登录
1:发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {//使用工具类判断手机号是否有效if (RegexUtils.isPhoneInvalid(phone)) {//无效返回错误信息return Result.fail("手机号格式错误");}//使用hutool包中的生成随机数的方法生成一个6位的验证码String code = RandomUtil.randomNumbers(6);//向返回的session中添加验证码信息session.setAttribute("code",code);//模拟发送验证码log.debug("验证码发送成功,{}",code);//发送成功返回return Result.ok();}
2:登录
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {//检查手机号是否有效,因为可能发送验证码时是正确的,后面又更改了if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {return Result.fail("手机号格式错误");}//进行验证码的校验String code = (String) session.getAttribute("code");//获取session中的验证码String code1 = loginForm.getCode();//获取用户输入的验证码if (code1==null||!code1.equals(code)){//比较是否相同return Result.fail("验证码输入错误");}//查找用户,看是否存在,从而判断是注册还是登录String phone = loginForm.getPhone();//获取手机号User user = lambdaQuery().eq(User::getPhone, phone).one();//通过mp进行单表查询,条件是手机号相同if (user==null){user= createUserWithPhone(phone);//用户为空,说明是注册,我们调用一个自定义方法来保存返回这个用户}//将用户保存到session中session.setAttribute("user",user);return Result.ok();
}private User createUserWithPhone(String phone) {User user = new User();//创建用户user.setPhone(phone);user.setNickName("user_"+RandomUtil.randomString(6));//获取一个随机用户名save(user);//mp方法直接保存return user;
}
校验验证码-》查询用户-》注册/登录-》保存到session
3:登录验证拦截器
1:自定义拦截器
//自定义的拦截器要实现HandlerInterceptor接口
public class LoginInterceptor implements HandlerInterceptor {//HandlerInterceptor中有三个可以重写的方法:拦截前处理,中,后@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取sessionHttpSession session = request.getSession();//获取session中的用户User user = (User) session.getAttribute("user");//判断用户是否存在if (user==null){//用户不存在不放行,返回状态码401:权限不足response.setStatus(401);return false;}//属性拷贝user->dtoUserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);//将用户信息放置到线程中;UserHolder.saveUser(userDTO);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//放止内存泄露,拦截结束后将用户从线程中删除UserHolder.removeUser();}
}
2:注册拦截器:
//配置类加上注解Configuration,然后注册拦截器就要使用WebMvcConfigurer中的方法,所以我们先实现了
@Configuration
public class MVCConfig implements WebMvcConfigurer {@Override//添加拦截器的方法public void addInterceptors(InterceptorRegistry registry) {//添加拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");//指定不许拦截的路径}
}
4:隐藏用户敏感信息
//转成dto隐藏用户信息
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
二:session的集群共享问题
因为session是存储在tomcat服务器中的,我们将来部署集群进行负载均衡,那么我们存储在一台tomcat服务器的数据无法实现共享;
我们考虑采用redis:1:共享数据;2:内存存储;3:键值结构
三:基于redis实现短信登录
我们以手机号为key,验证码为value保存到redis,然后后面将用户保存到redis使用hash来保存;
用户信息的key使用一个token来保存,一个是保证key的唯一性,一个是保证数据的安全性,因为token也就是key是要存在浏览器中的;要保证安全性;
我们做的修改:
在发送验证码时:
@Override
public Result sendCode(String phone, HttpSession session) {//使用工具类判断手机号是否有效if (RegexUtils.isPhoneInvalid(phone)) {//无效返回错误信息return Result.fail("手机号格式错误");}//使用hutool包中的生成随机数的方法生成一个6位的验证码String code = RandomUtil.randomNumbers(6);//session.setAttribute("code",code);//将验证码保存到redis中,手机号为key,验证码为value,并且设置有效期为2分钟redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);//模拟发送验证码log.debug("验证码发送成功,{}",code);//发送成功返回return Result.ok();}
在登录验证时:
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//检查手机号是否有效,因为可能发送验证码时是正确的,后面又更改了if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {return Result.fail("手机号格式错误");}//进行验证码的校验
// String code = (String) session.getAttribute("code");String code1 = loginForm.getCode();//获取用户输入的验证码String phone = loginForm.getPhone();//获取手机号String code = redisTemplate.opsForValue().get(phone);if (code1==null||!code1.equals(code)){//比较是否相同return Result.fail("验证码输入错误");}//查找用户,看是否存在,从而判断是注册还是登录User user = lambdaQuery().eq(User::getPhone, phone).one();//通过mp进行单表查询,条件是手机号相同if (user==null){user= createUserWithPhone(phone);//用户为空,说明是注册,我们调用一个自定义方法来保存返回这个用户}//转成dto隐藏用户信息UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// session.setAttribute("user",userDTO);//随机生成token,使用uuid生成,然后转成字符串String token = UUID.randomUUID().toString(true);//将对象转成map,使用beanutil的方法,用于后面给hash赋值Map<String, Object> map = BeanUtil.beanToMap(userDTO);//将用户存储在hash中,key为token,然后putall加入多个字段是map,我们前面创建过了;redisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY+token,map);//设置过期时间redisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,30,TimeUnit.MINUTES);return Result.ok(token);}
拦截器中的操作:
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// HttpSession session = request.getSession();//从请求头中获取tokenString token = request.getHeader("authorization");//从redis中根据token取出map;Map<Object, Object> usermap = redisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// UserDTO userDTO = (UserDTO) session.getAttribute("user");//判断map是否为空,不用判断是否为null,因为上面的方法entries会帮我们判断if (usermap.isEmpty()){//用户不存在不放行,返回状态码401:权限不足response.setStatus(401);return false;}//将map通过beanutil中的方法转成对象,fillBeanWithMap(usermap, new UserDTO(), false),第二个参数是//对象类型,第三个是是否抛出异常;UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//将用户信息放置到线程中;UserHolder.saveUser(userDTO);//刷新token的有效值:只要用户一直访问token一直存在就不需要重新登录获取token,超过刷新时间就需要重新登录redisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return true;}
登录的刷新问题
我们之前只有一个拦截器,但是拦截的路径不是所有,如果用户一直访问的是不需要拦截的路径,那么他的token就不会刷新,就会失去登录状态,我们可以再加一个拦截器,第一个拦截一切路径,并且刷新有效期,第二个做登录校验;
我们重新定义一个拦截器,无论与否都放行:
将拦截校验的操作交给第二个拦截器,
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// HttpSession session = request.getSession();//从请求头中获取tokenString token = request.getHeader("authorization");//从redis中根据token取出map;Map<Object, Object> usermap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);//判断map是否为空,不用判断是否为null,因为上面的方法entries会帮我们判断if (usermap.isEmpty()){//这个拦截器不做登录校验,直接放行;return true;}//将map通过beanutil中的方法转成对象,fillBeanWithMap(usermap, new UserDTO(), false),第二个参数是//对象类型,第三个是是否抛出异常;UserDTO userDTO = BeanUtil.fillBeanWithMap(usermap, new UserDTO(), false);//将用户信息放置到线程中;UserHolder.saveUser(userDTO);//刷新token的有效值:只要用户一直访问token一直存在就不需要重新登录获取token,超过刷新时间就需要重新登录stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return true;}
第二个拦截器:他判断是否登录的依据就是treadlocal中有没有用户:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {UserDTO user = UserHolder.getUser();if (StrUtil.isBlankIfStr(user)) {response.setStatus(401);return false;}return true;
}
别忘记注册拦截器:
public void addInterceptors(InterceptorRegistry registry) {//添加拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);//指定不许拦截的路径//order设置拦截器的先后顺序,order的值越小,拦截器越先执行;registry.addInterceptor(new ReFlashTokenINterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}