qidao123.com技术社区-IT企服评测·应用市场

标题: 【技能派面试篇】技能派微信公众号自动登录技能面试要点全解析 [打印本页]

作者: 钜形不锈钢水箱    时间: 2025-5-10 11:04
标题: 【技能派面试篇】技能派微信公众号自动登录技能面试要点全解析
你好,欢迎来到本次关于技能派微信公众号自动登录技能的面试系列分享。在这篇文章中,我们将深入探讨这一技能范畴的相关面试题推测。若想对相关内容有更透彻的理解,强烈推荐参考之前发布的博文:【技能派后端篇】技能派微信公众号自动登录技能 、【技能派后端篇】技能派中 Session/Cookie 与 JWT 身份验证技能的应用及实现解析。
焦点技能原理概述:系统通过验证码与前端建立并维持半长链接映射关系。当用户扫描二维码关注公众号并输入接收到的验证码后,系统会接收到微信的回调信息,进而辨认用户信息,并精准找到对应的半长链接,最终实现系统的自动登录功能。
1. 请描述验证码登录的完整流程设计?


以下是对微信公众号自动登录流程的具体梳理:
2. 怎样实现验证码与前端的长连接映射?


  1. LoadingCache<String, String> deviceCodeCache; // 实现设备ID到验证码的映射
  2. LoadingCache<String, SseEmitter> verifyCodeCache; // 实现验证码到SSE连接的映射
复制代码

  1. String code = CodeGenerateUtil.genCode(cnt++);
复制代码

  1. // 建立SSE连接,设置超时时间为15分钟(900_000毫秒)
  2. SseEmitter sseEmitter = new SseEmitter(900_000L);
  3. // 定义超时回调函数,当连接超时时清理缓存并完成连接
  4. sseEmitter.onTimeout(() -> {
  5.     verifyCodeCache.invalidate(realCode);
  6.     sseEmitter.complete();
  7. });
  8. // 将验证码与对应的SSE连接存入缓存
  9. verifyCodeCache.put(realCode, sseEmitter);
复制代码

  1. // 微信回调时,自动注册用户信息
  2. sessionService.autoRegisterWxUserInfo(fromUser);
  3. // 对输入的验证码进行校验,校验通过后触发登录操作
  4. qrLoginHelper.login(code);
复制代码
3. 怎样包管验证码系统的安全性?

为了确保验证码系统的安全性,接纳了以下多方面的措施,具体实现代码如下:
  1. // 定义验证码与SSE连接的缓存,设置最大容量为300,写入后5分钟过期
  2. private LoadingCache<String, SseEmitter> verifyCodeCache = CacheBuilder.newBuilder().maximumSize(300).expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<String, SseEmitter>() {
  3. });
  4. // 定义设备ID与验证码的缓存,设置最大容量为300,写入后5分钟过期
  5. private LoadingCache<String, String> deviceCodeCache = CacheBuilder.newBuilder().maximumSize(300).expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<String, String>() {
  6. });
复制代码

  1. // 登录校验方法,验证验证码的有效性并确保单次使用
  2. public boolean login(String verifyCode) {
  3.     SseEmitter sseEmitter = verifyCodeCache.getIfPresent(verifyCode);
  4.     if (sseEmitter == null) return false; // 若验证码不存在或已失效,返回false
  5.     verifyCodeCache.invalidate(verifyCode); // 将已使用的验证码从缓存中移除,确保单次有效
  6.     return true;
  7. }
复制代码
4. 为什么选择SSE而非WebSocket?

在技能选型过程中,对于服务器向客户端推送登录状态这一需求,SSE(Server-Sent Events)相较于WebSocket具有更合适的特性。SSE专门设计用于实现单向的服务器到客户端的消息推送,非常符合我们仅需要服务器推送登录状态的场景。而WebSocket则更侧重于双向通信,提供了更复杂的全双工通信功能。因此,综合思量项目需求和技能特点,选择SSE能够更简洁、高效地满意系统的功能要求,同时降低不必要的复杂性和开辟本钱。
5. 怎样处理高并发下的SSE连接?

在高并发场景下,为了确保SSE连接的稳固运行和系统资源的有效利用,接纳了以下处理策略,相关代码如下:
  1. // 在WxLoginHelper类中定义验证码与SSE连接的缓存,设置最大容量为300,写入后5分钟过期
  2. private LoadingCache<String, SseEmitter> verifyCodeCache = CacheBuilder.newBuilder().maximumSize(300).expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<String, SseEmitter>() {
  3. });
复制代码
  1. // 定义SSE连接的异常处理机制,包括超时和错误处理
  2. sseEmitter.onTimeout(() -> {
  3.     log.info("sse 超时中断 --> {}", realCode);
  4.     verifyCodeCache.invalidate(realCode);
  5.     sseEmitter.complete();
  6. });
  7. sseEmitter.onError((e) -> {
  8.     log.warn("sse error! --> {}", realCode, e);
  9.     verifyCodeCache.invalidate(realCode);
  10.     sseEmitter.complete();
  11. });
复制代码

6. 怎样实现用户自动注册?

用户自动注册流程通过以下步骤实现:

  1. // 自动注册逻辑(WxCallbackRestController.java:69)
  2. sessionService.autoRegisterWxUserInfo(fromUser);
复制代码

  1. /**
  2.   * 没有注册时,先注册一个用户;若已经有,则登录
  3.   *
  4.   * @param req
  5.   */
  6. private Long registerOrGetUserInfo(UserSaveReq req) {
  7.      UserDO user = userDao.getByThirdAccountId(req.getThirdAccountId());
  8.      if (user == null) {
  9.          return registerService.registerByWechat(req.getThirdAccountId());
  10.      }
  11.      return user.getId();
  12. }
复制代码

  1.     @Override
  2.     @Transactional(rollbackFor = Exception.class)
  3.     public Long registerByWechat(String thirdAccount) {
  4.         // 用户不存在,则需要注册
  5.         // 1. 保存用户登录信息
  6.         UserDO user = new UserDO();
  7.         user.setThirdAccountId(thirdAccount);
  8.         user.setLoginType(LoginTypeEnum.WECHAT.getType());
  9.         userDao.saveUser(user);
  10.         // 2. 初始化用户信息,随机生成用户昵称 + 头像
  11.         UserInfoDO userInfo = new UserInfoDO();
  12.         userInfo.setUserId(user.getId());
  13.         userInfo.setUserName(UserRandomGenHelper.genNickName());
  14.         userInfo.setPhoto(UserRandomGenHelper.genAvatar());
  15.         userDao.save(userInfo);
  16.         // 3. 保存ai相互信息
  17.         UserAiDO userAiDO = UserAiConverter.initAi(user.getId());
  18.         userAiDao.saveOrUpdateAiBindInfo(userAiDO, null);
  19.         processAfterUserRegister(user.getId());
  20.         return user.getId();
  21.     }
复制代码

  1. qrLoginHelper.login(code);
复制代码
7. 怎样实现基于Cookie和JWT的会话管理?


  1.   /**
  2.    * 微信公众号登录
  3.    *
  4.    * @param verifyCode 用户输入的登录验证码
  5.    * @return
  6.    */
  7.   public boolean login(String verifyCode) {
  8.       // 通过验证码找到对应的长连接
  9.       SseEmitter sseEmitter = verifyCodeCache.getIfPresent(verifyCode);
  10.       if (sseEmitter == null) {
  11.           return false;
  12.       }
  13.       String session = sessionService.loginByWx(ReqInfoContext.getReqInfo().getUserId());
  14.       try {
  15.           // 登录成功,写入session
  16.           sseEmitter.send(session);
  17.           // 设置cookie的路径
  18.           sseEmitter.send("login#" + LoginService.SESSION_KEY + "=" + session + ";path=/;");
  19.           return true;
  20.       } catch (Exception e) {
  21.           log.error("登录异常: {}", verifyCode, e);
  22.       } finally {
  23.           sseEmitter.complete();
  24.           verifyCodeCache.invalidate(verifyCode);
  25.       }
  26.       return false;
  27.   }
复制代码

  1.     public String genSession(Long userId) {
  2.         // 1.生成jwt格式的会话,内部持有有效期,用户信息
  3.         String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId));
  4.         String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire()))
  5.                 .withPayload(session)
  6.                 .sign(algorithm);
  7.         // 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息
  8.         // 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定
  9.         RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000);
  10.         return token;
  11.     }
复制代码

  1. // 设置cookie的路径
  2. sseEmitter.send("login#" + LoginService.SESSION_KEY + "=" + session + ";path=/;");
  3. // 前端登录成功后设置Cookie:
  4. document.cookie = "f-session=" + sessionToken;
复制代码
8. 怎样实现分布式Session管理?


  1. public void removeSession(String session) {
  2.     RedisClient.del(session);  // 清除Redis映射
  3.     // JWT本身仍有效但无法通过校验
  4. }
复制代码
9.怎样实现接口权限控制?


  1. @Target({ElementType.METHOD, ElementType.TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Permission {
  5.     /**
  6.      * 限定权限
  7.      *
  8.      * @return
  9.      */
  10.     UserRole role() default UserRole.ALL;
  11. }
复制代码

  1. public enum UserRole {
  2.     ADMIN,    // 管理员
  3.     LOGIN,    // 登录用户
  4.     ALL;      // 所有用户
  5. }
复制代码

  1. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  2.         if (handler instanceof HandlerMethod) {
  3.             HandlerMethod handlerMethod = (HandlerMethod) handler;
  4.             Permission permission = handlerMethod.getMethod().getAnnotation(Permission.class);
  5.             if (permission == null) {
  6.                 permission = handlerMethod.getBeanType().getAnnotation(Permission.class);
  7.             }
  8.             if (permission == null || permission.role() == UserRole.ALL) {
  9.                 if (ReqInfoContext.getReqInfo() != null) {
  10.                     // 用户活跃度更新
  11.                     SpringUtil.getBean(UserActivityRankService.class).addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setPath(ReqInfoContext.getReqInfo().getPath()));
  12.                 }
  13.                 return true;
  14.             }
  15.             if (ReqInfoContext.getReqInfo() == null || ReqInfoContext.getReqInfo().getUserId() == null) {
  16.                 if (handlerMethod.getMethod().getAnnotation(ResponseBody.class) != null
  17.                         || handlerMethod.getMethod().getDeclaringClass().getAnnotation(RestController.class) != null) {
  18.                     // 访问需要登录的rest接口
  19.                     response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
  20.                     response.getWriter().println(JsonUtil.toStr(ResVo.fail(StatusEnum.FORBID_NOTLOGIN)));
  21.                     response.getWriter().flush();
  22.                     return false;
  23.                 } else if (request.getRequestURI().startsWith("/api/admin/") || request.getRequestURI().startsWith("/admin/")) {
  24.                     response.sendRedirect("/admin");
  25.                 } else {
  26.                     // 访问需要登录的页面时,直接跳转到登录界面
  27.                     response.sendRedirect("/");
  28.                 }
  29.                 return false;
  30.             }
  31.             if (permission.role() == UserRole.ADMIN && !UserRole.ADMIN.name().equalsIgnoreCase(ReqInfoContext.getReqInfo().getUser().getRole())) {
  32.                 // 设置为无权限
  33.                 response.setStatus(HttpStatus.FORBIDDEN.value());
  34.                 return false;
  35.             }
  36.         }
  37.         return true;
  38.     }
复制代码

  1. @Permission(role = UserRole.LOGIN)
  2. @GetMapping("logout")
  3. public ResVo<Boolean> logOut(HttpServletResponse response) {
  4. }
复制代码

10. 如果要做分布式摆设,需要改造哪些模块?

当系统需要举行分布式摆设时,为了顺应分布式情况的特点和要求,需要对以下模块举行改造:

通过以上对微信公众号自动登录技能的全面分析和具体介绍,盼望能够帮助你更好地理解这一技能范畴的焦点要点和面试考察方向,祝你在面试中取得优异的结果!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/) Powered by Discuz! X3.4