基于上述思路,一共有三个接口需要完成,一是发送短信验证码,二是短信验证码注册,登录,三是校验登录状态,我将会逐一的从Controller层,Service层进行讲解。
1.发送短信验证码
···java
---UserConrtroller
//先进行依赖注入,每一步都不要忘记
@Resource
private IUserService userService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
//发送短信验证码并保存验证码
//参数中需要session的原因是一会需要将验证码保存在session当中
return userService.sendCode(phone,session);
}
---UserServiceImpl
@Resource
private StringRedisTemplate stringRedisTemplate;
//发送验证码的方法,在发送验证码之前需要先验证一下手机号是否符合规则
@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,便于后期的比较,用手机号当key可以保证唯一性
//加上业务前缀,让结构更清晰,知道这里保存的信息是用来干什么的
//存入的验证码需要设置过期时间,因为如果不设置,那么redis早晚有一天会被填满
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.发送验证码,由于真实的发送短信验证码需要借助第三方软件,由于这里并不是重点,所以我们一带而过,用日志来代替我们的
//真正发送那个验证码的这个环节
log.debug("发送短信验证码成功,验证码是{}",code);
return Result.ok();
}
```
这里发送验证码的逻辑还是较为简单的,发送验证码的前提是手机号格式是正确的,所以在发送验证码之前,用一个工具类来检验一下接收到的手机号即可,不光要发送,还要保存到redis当中,便于后续和前端传来的验证码进行比较,并且给这个验证码设置有效期。
2.短信验证码登录,注册
```java
---UserController
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
//因为前端发送的数据是json风格的,所以我们想要接收json风格的数据就需要使用RequestBody这个注解
//实现登录功能
return userService.login(loginForm,session);
}
---UserServiceImpl
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校验手机号 因为在接收到验证码之后,用户可以再更改手机号,如果不再检查一遍,用户可能
//可能就会利用漏洞
if(RegexUtils.isPhoneInvalid(phone)){
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
//2.从redis获取验证码并校验
String cacheCode =stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
String code = loginForm.getCode();
//将不满足的条件排除,因为只要有一个不满足就会报错,而满足的情况有很多,会形成if嵌套,使代码冗余
if(cacheCode==null ||!cacheCode.equals(code)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//4.一致,根据手机号查询用户 select * form tb_user where phone =?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user==null){
//6.不存在,创建新用户并保存用户到数据库
user=createUserWithPhone(phone);
}
//7.保存用户信息到redis 注意这里的工具类的拼写
//7.1 随机生成token保存到令牌 true是设置为简单,也就是不带中划线的uuid
String token = UUID.randomUUID().toString(true);
//7.2 先将user对象转为userDTO对象,再将UserDTO对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//转为userDTO是因为我们并不是所有的信息都需要向外展示,要防止信息泄露
//再转为map是因为putAll方法需要我们key和value的值都为string类型,才能进行存储
//所以我们可以借助map集合进行更改
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create().
setIgnoreNullValue(true).
setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
//7.3 存储 通过上面将userDTO转化为一个Map集合后,就可以通过putAll方法,将一整个集合存入,简化了代码
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
//7.4设置token有效期,如果这里不设置有效期,就会和加入验证码那里一样,都会被填满,所以我们设置如果30分钟不进行操作,就会被删除
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
//8.将token返回给前端
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX +RandomUtil.randomString(10));
save(user);
return user;
}
```
3.校验登录状态
校验登录状态,无非就是当前用户是否处于登录状态,有一些请求是需要登录请求,而有些方法并不需要登录请求,所以此时我们需要设置两个拦截器,一个负责刷新token,一个负责验证是否处于登录状态。
```java
//刷新拦截器
public class RefrushTokenInterceptor implements HandlerInterceptor {
//这里不能使用依赖注入,因为这个类并不是由spring创建的,而是由我们自己new出来的,由spring创建出来的对象我们才能使用依赖注入,
//由MvcConfig中创建此类的对象时,传入的stringRedisTemplate拿到
private StringRedisTemplate stringRedisTemplate;
public RefrushTokenInterceptor(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)) {
return true;
}
//2.基于token获取redis当中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(LOGIN_USER_KEY + token);
//3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
//5.将查询到的Hash数据转化为DTO对象,因为只有转化为DTO对象,才能将用户信息保存到ThreadLocal当中
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,将用户信息保存到ThreadLocal ,这里的UserHolder实现了ThreadLocal的方法,封装的实现了,比较方便
UserHolder.saveUser(userDTO);
//7/刷新token的有效期,因为发送请求会先被拦截器拦截,只要还在执行任何需要登录的操作,我们就让他一直刷新token,这样证明了它一直在
//被使用
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}
//登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
//用户登录之前做校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser()==null) {
//没有,设置状态码并拦截
response.setStatus(401);
return false;
}
//有用户则放行
return true;
}
}
//要想让拦截器生效,需要配置拦截器
//配置类需要这个注解,别忘了
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
//添加拦截器的方法,形参的registry是拦截器的注册器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//这里我们不光需要设置拦截器,因为并不是所有的接口的请求都需要拦截,所以我们要排除一些不需要拦截的接口
//登录拦截器
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
//拦截所有请求
//token刷新的拦截器
registry.addInterceptor(new RefrushTokenInterceptor(stringRedisTemplate)).order(0);
}
}
```
两个拦截器当中的stringRedisTemplate都是都配置类中获取的,原因就是当前用于刷新的拦截器是我们自己new出来的,不是由spring创建的。两个拦截器有先后顺序,先走登录刷新拦截器,再走登录拦截器,用order去设置它们的执行顺序。只要有操作说明当前用户一直处于登录状态,这样就保证了即使用户并没有执行需要登录校验的操作,比如查看首页等等,用户的登录也不会过期。