• 黑马点评-短信登录业务


    原理

    模型如下

    nginx

    nginx基于七层模型走的事HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量。

    我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发。

    经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。

    数据存储

    在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右。

    所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。

    登录流程

    首先用户点发送验证码->验证码存入session

    用户点登录或注册->检查+处理用户信息->用户信息存入session

    用户请求(携带cookie)->cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息->检查用户是否存在来拦截

    拦截功能

    1. //1.获取session
    2. HttpSession session = request.getSession();
    3. //2.获取session中的用户
    4. Object user = session.getAttribute("user");
    5. //3.判断用户是否存在
    6. if(user == null){
    7. //4.不存在,拦截,返回401状态码
    8. response.setStatus(401);
    9. return false;
    10. }
    11. //5.存在,保存用户信息到Threadlocal
    12. UserHolder.saveUser((User)user);
    13. //6.放行
    14. return true;
    15. //拦截器
    16. registry.addInterceptor(new LoginInterceptor())
    17. .excludePathPatterns(
    18. "/shop/**",
    19. "/voucher/**",
    20. "/shop-type/**",
    21. "/upload/**",
    22. "/blog/hot",
    23. "/user/code",
    24. "/user/login"
    25. ).order(1);
    26. // token刷新的拦截器
    27. registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);

    Tomcat原理

    当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外。当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket互相传递数据。

    当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求。

    我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB。

    在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应。

    隐藏敏感信息

    核心思路就是写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象

    1. public static void saveUser(UserDTO user){
    2. tl.set(user);
    3. }
    4. public static UserDTO getUser(){
    5. return tl.get();
    6. }
    7. public static void removeUser(){
    8. tl.remove();
    9. }
    10. //保存用户信息到session中
    11. session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
    12. //存在,保存用户信息到Threadlocal
    13. UserHolder.saveUser((UserDTO) user);

    Redis代替session

    由于在第二台服务器上,没有第一台服务器存放的session

    早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session

    这样会造成每台服务器中都有完整的一份session数据,服务器压力过大,下面用redis优化

    session是每个用户都有自己的session,但是redis的key是共享的,因此可以解决session共享问题

    在设计这个key的时候,需要满足key具有唯一性且方便携带,选择在后台生成一个随机串token

    1. @Override
    2. public Result login(LoginFormDTO loginForm, HttpSession session) {
    3. // 1.校验手机号
    4. String phone = loginForm.getPhone();
    5. if (RegexUtils.isPhoneInvalid(phone)) {
    6. // 2.如果不符合,返回错误信息
    7. return Result.fail("手机号格式错误!");
    8. }
    9. // 3.从redis获取验证码并校验
    10. String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    11. String code = loginForm.getCode();
    12. if (cacheCode == null || !cacheCode.equals(code)) {
    13. // 不一致,报错
    14. return Result.fail("验证码错误");
    15. }
    16. // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
    17. User user = query().eq("phone", phone).one();
    18. // 5.判断用户是否存在
    19. if (user == null) {
    20. // 6.不存在,创建新用户并保存
    21. user = createUserWithPhone(phone);
    22. }
    23. // 7.保存用户信息到 redis中
    24. // 7.1.随机生成token,作为登录令牌
    25. String token = UUID.randomUUID().toString(true);
    26. // 7.2.将User对象转为HashMap存储
    27. UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    28. Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
    29. CopyOptions.create()
    30. .setIgnoreNullValue(true)
    31. .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    32. // 7.3.存储
    33. String tokenKey = LOGIN_USER_KEY + token;
    34. stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    35. // 7.4.设置token有效期
    36. stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    37. // 8.返回token
    38. return Result.ok(token);
    39. }

    登录状态刷新

    由于拦截器绑定了刷新登录token令牌的存活时间,如果当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行。

    当前方案如下:

    解决方案:添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可。

    1. // 第一个拦截器
    2. public class RefreshTokenInterceptor implements HandlerInterceptor {
    3. private StringRedisTemplate stringRedisTemplate;
    4. public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
    5. this.stringRedisTemplate = stringRedisTemplate;
    6. }
    7. @Override
    8. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    9. // 1.获取请求头中的token
    10. String token = request.getHeader("authorization");
    11. if (StrUtil.isBlank(token)) {
    12. return true;
    13. }
    14. // 2.基于TOKEN获取redis中的用户
    15. String key = LOGIN_USER_KEY + token;
    16. Map userMap = stringRedisTemplate.opsForHash().entries(key);
    17. // 3.判断用户是否存在
    18. if (userMap.isEmpty()) {
    19. return true;
    20. }
    21. // 5.将查询到的hash数据转为UserDTO
    22. UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    23. // 6.存在,保存用户信息到 ThreadLocal
    24. UserHolder.saveUser(userDTO);
    25. // 7.刷新token有效期
    26. stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
    27. // 8.放行
    28. return true;
    29. }
    30. @Override
    31. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    32. // 移除用户
    33. UserHolder.removeUser();
    34. }
    35. }
    36. // 第二个拦截器
    37. public class LoginInterceptor implements HandlerInterceptor {
    38. @Override
    39. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    40. // 1.判断是否需要拦截(ThreadLocal中是否有用户)
    41. if (UserHolder.getUser() == null) {
    42. // 没有,需要拦截,设置状态码
    43. response.setStatus(401);
    44. // 拦截
    45. return false;
    46. }
    47. // 有用户,则放行
    48. return true;
    49. }
    50. }

    总结

    这节主要分析了项目的整体架构,redis部分处理了短信验证码和登录状态验证(代替session)

  • 相关阅读:
    自动跟踪太阳光电路设计
    zabbix部署和简单使用
    [Spring Cloud] gateway全局异常捕捉统一返回值
    手把手教你VScode终端自动激活anaconda的python虚拟环境
    【Linux基础】工作中常用的linux命令,经常会被面试官问到
    freemarker模板引擎详解以及使用方法
    【多式联运】基于帝国企鹅算法+遗传算法+粒子群算法求解不确定多式联运路径优化问题【含Matlab源码 2073期】
    智慧旅游+数字化景区整体解决方案:文件全文83页,附下载
    Redis基本使用
    Verilog写状态机的三种描述方式之二段式
  • 原文地址:https://blog.csdn.net/cangshanjiang/article/details/136405512