发送验证码、短信验证码登陆、注册在后端,校验登陆在springmvc的连接器中,根据请求携带cookie来确定找到session
短信验证登陆与注册新用户:
/**
* 发送验证码
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、手机号合法,生成验证码,并保存到Session中
String code = RandomUtil.randomNumbers(6);
session.setAttribute(SystemConstants.VERIFY_CODE, code);
// 3、发送验证码
log.info("验证码:{}", code);
return Result.ok();
}
/**
* 用户登录
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、判断验证码是否正确
String sessionCode = (String) session.getAttribute(LOGIN_CODE);
if (code == null || !code.equals(sessionCode)) {
return Result.fail("验证码不正确");
}
// 3、判断手机号是否是已存在的用户
User user = this.getOne(new LambdaQueryWrapper()
.eq(User::getPassword, phone));
if (Objects.isNull(user)) {
// 用户不存在,需要注册
user = createUserWithPhone(phone);
}
// 4、保存用户信息到Session中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
session.setAttribute(LOGIN_USER, user);
return Result.ok();
}
/**
* 根据手机号创建用户
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}
登陆拦截器:
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置拦截器,用于判断用户是否登录
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
// 1、判断用户是否存在
User user = (User) session.getAttribute(LOGIN_USER);
if (Objects.isNull(user)){
// 用户不存在,直接拦截
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
return false;
}
// 2、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理
// 比如:方便获取和使用用户信息,session获取用户信息是具有侵入性的
ThreadLocalUtls.saveUser(user);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
存在问题就是,如果是多个服务器都提供了登录和其他服务功能,nigix反向代理会找到不同的主机中,这时候,这个时候就需要用户再次登录了:
解决方法:
1、session共享,利用拷贝功能,但是数据同步需要时间,而且占用内存
2、使用redis缓存来实现共享session问题,问题的关键是怎么获得获取redis中对象时,需要自己手动设置一个key,下次查询也需要携带这个令牌,这个时候前端就是会把返回的token保存在web服务器中,保证每次都是携带了token的
使用redis缓存验证码,key需要时唯一的,可以采用电话号码就可以,值得话直接也是string格式
1、采用string形式,相当于把对象序列化为JOSN字符串,但是修改起来需要整体删除了在添加
2、采用的是redis中hash格式,这个格式中有一个feild和一个calue值,相当于一个hashmap,修改起来方面,可以随意添加新的对象属性。相当于对单个字段的CRUD更加灵活。
使用redis缓存对象的key怎么保证唯一性:
自己设置一个业务作为前缀,然后拼接一个UUID来保证key的唯一性。
短信验证登陆:
/**
* 发送验证码
*
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、手机号合法,生成验证码,并保存到Redis中
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code,
RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 3、发送验证码
log.info("验证码:{}", code);
return Result.ok();
}
/**
* 用户登录
*
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、判断验证码是否正确
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (code == null || !code.equals(redisCode)) {
return Result.fail("验证码不正确");
}
// 3、判断手机号是否是已存在的用户
User user = this.getOne(new LambdaQueryWrapper()
.eq(User::getPhone, phone));
if (Objects.isNull(user)) {
// 用户不存在,需要注册
user = createUserWithPhone(phone);
}
// 4、保存用户信息到Redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 将对象中字段全部转成string类型,StringRedisTemplate只能存字符串类型的数据
Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).
setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
String token = UUID.randomUUID().toString(true);
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
/**
* 根据手机号创建用户并保存
*
* @param phone
* @return
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}
单独配置一个拦截器用户刷新Redis中的token:我们之前设置的登陆拦截器只是简单的对需要用户身份的设置了拦截器。但是在这里,为了防止redis中的用户信息一直存在,我们在上面代码中其实的给token设置了过期时间的,但是如果我们一直没用用到用户信息的功能,登录状态就会没有。因此,我们需要单独设置一个token的刷新拦截器,让每一次请求都会刷新token的有效期。
刷新token的拦截器:
public class RefreshTokenInterceptor implements HandlerInterceptor {
// new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)
// 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、获取token,并判断token是否存在
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
// token不存在,说明当前用户未登录,不需要刷新直接放行
return true;
}
// 2、判断用户是否存在
String tokenKey = LOGIN_USER_KEY + token;
Map
1、先获取token,如果查询到redis中存在,获取redis中的对象,将map结构使用beantil的copyproperties转为实体对象,并保存在threadlocal中。这里使用threadlocal是线程域对象,因此是线程隔离的
3、保存对象数据后,刷星token的过期时间,说明有人用过了
2、如果没有这个key,说明没有登陆,只需要放行就可以以
至此,使用redis共享session成功实现。