短信登录实现(黑马点评为例)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 短信登录实现(黑马点评为例)



前言

短信登录核心知识

首先黑马点评这个短信登录是一伪验证,即后台调用工具类随机生成六位数字。

1.Redis代替session共享,满足多tomcat共享数据。

2.登录拦截、线程隔离实现每个线程操控自己线程的数据与登录状态刷新问题解决。

3.创建新对象解决隐藏用户敏感信息。


一、隐藏用户敏感信息

  • 首先讲诉创建新对象解决隐藏用户敏感信息的问题,方便后面讲解。
  • 思考:正常登录成功后,我们通过浏览器观察到此时用户的全部信息都在,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏。
  • 解决:采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了。(二次封装,但是此次封装对象只包含一些用户简单粗略的信息如头像,用户名等。)
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

在UserHolder处:将所有User对象换成UserDTO

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    public static void saveUser(UserDTO user){
        tl.set(user);
    }
    public static UserDTO getUser(){
        return tl.get();
    }
    public static void removeUser(){
        tl.remove();
    }
}

这里分UserHolder是一个将用户对象存储到线程中的类并包含获取当前线程用户对象和删除方的法。下面会讲解。

二、短信验证登录、注册

1.流程

基于session实现:

发送验证码——>短信验证登录(未注册自动注册)——>校验登录状态

  • 发送验证码:用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号。如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用。
  • 短信验证登录、注册:用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息。
  • 校验登录状态:用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行。

2.代码

发送验证码

@Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码(工具类)
        String code = RandomUtil.randomNumbers(6);
        // 4.保存验证码到 session
        session.setAttribute("code",code);
        // 5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

登录(校验、登录和注册)

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.toString().equals(code)){
             //3.不一致,报错
            return Result.fail("验证码错误");
        }
        //一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if(user == null){
            //不存在,则创建
            user =  createUserWithPhone(phone);
        }
        //7.保存用户信息到session中
        session.setAttribute("user",user);
        return Result.ok();
    }
private User createUserWithPhone(String phone) {
        // 1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString((10)));
        // 2.保存用户
        save(user);
        return user;
    }

3.使用redis优化

  • 分析:
    每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了。
  • 问题:
  • 1、每台服务器中都有完整的一份session数据,服务器压力过大。
  • 2、session拷贝数据时,可能会出现延迟。
  • 解决:redis代替session使用Hash结构存储对象

解决

  • 当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
  • 采用token作为key:
  • 让key具有唯一性,这样不同用户储存到redis的key唯一;
  • 方便携带,因为前端请求头中可以携带token
  • hash存储对象:
  • key-map结构,如上面图片所示,优点也如上图。

代码

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.从redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 不一致,报错
        return Result.fail("验证码错误");
    }
    // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
    User user = query().eq("phone", phone).one();
    // 5.判断用户是否存在
    if (user == null) {
        // 6.不存在,创建新用户并保存
        user = createUserWithPhone(phone);
    }
    // 7.保存用户信息到 redis中
    // 7.1.随机生成token,作为登录令牌
    String token = UUID.randomUUID().toString();
    // 7.2.将User对象转为HashMap存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    // (将Map集合中的非String类型转换为String)
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    // 7.3.存储
    String tokenKey = LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    // 7.4.设置token有效期,设置登录后用户权限有效期
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.返回token
    return Result.ok(token);
}

二、登录拦截(校验)

每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据。

温馨小贴士:tomcat的运行原理

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应。

1.流程

如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离

分析到这里,可以考虑一下:拦截实现了,上面的短信登录中有个token有效期,设置登录后用户权限有效期,就是让用户登录信息状态存在多久后过期,我们上面是从登录的那一刻算,30分钟后过期,可能需要再次验证登录才能操作一些具有权限的页面。其实按照实际我们应该是访问某个页面的时候,就应该重新刷新有效期,正常有效期存在不可能是用户在操作过程中突然失去登录信息,而是在多久不进行任何操作后失效,所有我们需要对拦截器进行优化,加入刷新token有效期的功能。

2.代码

拦截器:

public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public RefreshTokenInterceptor(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中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

让拦截器生效:配置类添加

注意设置优先级,应该让刷新拦截器最优先。

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @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 RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

总结

以上就是短信登录的详细实现,细节满满。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
数据采集 API 开发者
调用API接口获取小红书笔记详情数据(小红书怎么推广)
小红书平台对于其API的使用有严格的规定和限制,并且并非所有的功能和数据都通过公开API提供。关于获取小红书笔记详情的API,以下是一些建议和指导:
|
10月前
|
API PHP
漏刻有时API接口实战开发系列(9):短信宝短信接口php开发及调用
漏刻有时API接口实战开发系列(9):短信宝短信接口php开发及调用
54 0
|
10月前
|
Java Maven
集成一个以官网(微信,QQ,微博)为标准的登录分享功能
今天要分享的是一个老生常谈的一个功能,也是网上一搜一大片的技术点,没什么技术含量,就是整合一下,提供一下方便,相对于友盟,ShareSdk中夹杂着一些别的功能,此文封装的绝对纯净,除了官网所提供的,不夹杂任何的代码逻辑,登录就是登录,分享就是分享,实实在在的以官网为标准。
|
12月前
《阿里云产品手册2022-2023 版》——阿里邮箱
《阿里云产品手册2022-2023 版》——阿里邮箱
|
机器学习/深度学习 人工智能 架构师
|
搜索推荐 5G UED
5G消息应用号推荐|官方种草清单第二期
高考信息一手掌握,乡村服务线上体验,中小企业服务全新方式!5G消息应用号推荐官方种草清单第二期真的很Nice!快来小5一起体验!
5G消息应用号推荐|官方种草清单第二期
|
双11 开发者
有奖反馈 | 支付宝使用demo在线调试,小白也能轻松接入当面付
刚到公司一段时间就赶上双十一,大促在即,当面付的功能开发不知道怎么搞,奈何功能包接口太多,组合效果啥样子不清楚!能不能满足老板的“想象”还不知道!商家资质也没申请下来,环境搭建又很繁琐,文档写的还和教科书一样复杂,啊啊啊~有个demo可以让我先试试,学习一把就好了。开箱即用的当面付在线调试Demo玩起来,效果一目了然,这次我们还提供了大礼相送,学+实践+好礼,能力这趴不再是问题!
2222 0
有奖反馈 | 支付宝使用demo在线调试,小白也能轻松接入当面付
|
开发工具 UED
有奖调研 | 致云通信短信服务用户的一封信
感谢您一直以来对阿里云通信短信服务的支持。为了提升用户体验,为您在数字化转型的通信之路提供助力,云通信短信服务将发起一次满意度调研。有关短信服务,无论使用情况,抑或功能需求,还是文档、产品介绍页、计算与账单、控制台、API&SDK、售后服务,您都可以一并提交真实的反馈,这将为短信服务的发展注入最强的动力!
2315 0
有奖调研 | 致云通信短信服务用户的一封信
|
新零售 开发者
上支付宝搜索“支付宝开放日”报名,小程序年度峰会即将在杭开启!
支付宝开放日·小程序年度峰会将聚焦支付宝为代表的大阿里经济体小程序背后商业的思考、布局与进展,助力生态商家及合作伙伴商业发展,共创新商业生态,践行“让天下没有难做的生意”的使命。
421 0
上支付宝搜索“支付宝开放日”报名,小程序年度峰会即将在杭开启!
如何开发创建微信、支付宝、百度等多合一小程序?
那么要是能有一个平台,能一下子生成多个平台的小程序就好了,那么小编告诉你,阿里云已经帮你实现了。
9173 0