Web应用微信登录/绑定微信开发流程

参考链接:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

1.前置准备

首先需要登录微信开放平台https://open.weixin.qq.com/

登录成功后点击创建网站应用,进行网站应用的创建,主要需要填写的信息有:

  1. 填写微信开放平台网站信息登记表,包括网站信息、开发者账户信息以及负责人联系信息

网站需进行部署及备案,因为该表中需要填写网站网址以及网站备案/许可证号

  1. 填写网站的基本信息

填写完信息后提交审核,审核通过后可以看到创建好的网站应用:

这里需要记录好AppID,并生成AppSecret,后续开发中需要使用

2.前端代码

参考官方给出的开发文档,这里我们采用将微信登录二维码内嵌到自己页面的方式进行微信登陆

2.1 HTML版本

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Wechat Login</title>
  <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
</head>

<body>
  <div id="login_container"></div>
  <script>
    let uuid = generateUUID();
    let state = uuid + "_unregistered";
    let encodedState = encodeURIComponent(state);
    
    let obj = new WxLogin({
      self_redirect: false,
      id: "login_container",
      appid: "your_appid",
      scope: "snsapi_login",
      redirect_uri: "your_redirect_uri",
      state: encodedState,
      style: "black",
    });
    
    function generateUUID() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0,
          v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }

    
  </script>
</body>

</html>

主要就是通过wxLogin.js库来创建一个obj对象,即可将二维码加载到对应的div中,完整的参数说明见官方文档,下面对几个比较重要参数进行解释:

  1. self_redirect:建议设置为false,如果设置为true的话,会在 iframe 内跳转到 redirect_uri,可能造成格式上的问题;设置为false即可在 top window 跳转到 redirect_uri
  2. appid:你的网站应用的appid
  3. redirect_uri:重定向的地址,也就是后端处理登录逻辑的接口
  4. state:用于保持请求和回调的状态;可以用一个uuid来表示,然后在后端进行存储(例如redis,可以设置一定的存活时间),即可用于判断用户的登录是否过期。这里拼接了一个_unregistered,这是用户是否已经注册过的标识,后端也可以获取到这个值(通过_进行字符串分割,前面的是state,后面的就是该标识),用于区分是微信注册/登录还是已有的账号来绑定微信

2.2 Vue版本

<template>
    <div>
      <div id="loginContainer"></div>
    </div>
  </template>
  
  <script>
  import { onMounted} from 'vue';
  import {v4 as uuidv4} from 'uuid';

  export default {
    setup() {

      onMounted(() => {
        const uuid = uuidv4();
        const state = uuid + "_unregistered";
        const encodedState = encodeURIComponent(state);
  
        new WxLogin({
          self_redirect: false,
          id: "loginContainer",
          appid: "your_appid",
          scope: "snsapi_login",
          redirect_uri: "your_redirect_uri",
          state: encodedState,
          style: "black",
        });
      });
    },
  };
  </script>
  
  <style scoped>
  </style>

vue版本和HTML版本的逻辑基本一致,参数也是相同的,需要注意两点:

  1. uuid的生成可以通过npm install uuid来安装现有的库
  2. 需要添加wxLogin.js库:在index.html中的head中添加:<script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>

3.后端代码

3.1 参数定义

application.yml文件中:

# 微信登陆参数
wechat:
  appid: your-appid
  secret: your-appsecret
  redirectUri: https://your-domain/api/wechat/login

分别对appid, appsecret和redirectUri进行了定义

3.2 接口说明

endpoint 功能说明
/api/wechat/qrcode 生成微信登录二维码(需要注意的是, 如果使用将二维码内嵌到自己页面的方式, 该接口暂时用不到, 二维码的生成逻辑直接在前端代码中写好了)
/api/wechat/login 微信登录的逻辑处理接口, 用code换取access_token和通过access_token获取用户信息的逻辑都在该接口中
/api/wechat/isBind 查看用户是否绑定微信
/api/user/setUsernameAndPassword 设置用户名和密码, 用户通过微信注册时调用该接口来补全信息
/api/user/setEmail 设置邮箱, 用户通过微信注册时调用该接口来补全信息

3.3 代码说明

注意: 下面的代码说明中, 有关state的功能暂时不用理会, 暂不考虑用户登录状态的问题

  1. LoginController:

给出了生成登录二维码, 微信登录以及查询是否绑定微信三个接口的实现, 至于用户信息的设置接口就因人而异了

@RestController
@RequestMapping("/api")
public class LoginController {

    @Autowired
    private UserService userService;

    @Value("${wechat.appid}")
    private String appId;

    @Value("${wechat.secret}")
    private String appSecret;

    @Value("${wechat.redirectUri}")
    private String redirectUri;

    // 生成登录二维码
    @GetMapping("/wechat/qrcode")
    public ApiResponse<Object> getQRCodeUrl(@RequestParam(required = false) String userId) {
        String state = UUID.randomUUID().toString(); // 生成随机 state
        //判断用户是否注册 (userId即用户id)
        if (Objects.isNull(userId)) {
            userId = "unregistered";
        }
        // 生成二维码链接
        String url = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=" + appId +
                "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=" + state + "+" + userId;
        Map<String, String> result = new HashMap<>();
        result.put("qrcodeUrl", url);
        result.put("state", state);
        return ApiResponse.success("二维码生成成功", result);
    }

    //微信登录
    @GetMapping("/wechat/login")
    public void WechatLoginCallback(@RequestParam String code, @RequestParam String state,HttpServletResponse response) throws JsonProcessingException, UnsupportedEncodingException {
        // 获取access_token
        Map<String, Object> tokenResponse = userService.getAccessToken(code);
        String accessToken = (String) tokenResponse.get("access_token");
        String openId = (String) tokenResponse.get("openid");
        // 获取用户信息
        Map<String, Object> userInfo = userService.getUserInfo(accessToken, openId);
        //从用户信息中提取出昵称和头像
        String nickname = (String) userInfo.get("nickname");
        String header = (String) userInfo.get("headimgurl");
        // 生成jwt token
        String token = JwtUtil.generateToken(nickname, openId);
        // 将token放入返回结果中
        Map<String, Object> res = new HashMap<>();
        res.put("token", token);
        // 查询用户是否存在
        User user = userService.getUserByOpenId(openId);
        if (user == null) { // 用户不存在/未绑定微信
            String userId = state.split("_")[1];
            if ("unregistered".equals(userId)) {
                // 未注册
                User newUser = userService.registerWechat(openId, nickname, header);
                res.put("user", newUser); // 用户信息
                res.put("flag","register"); // 标识
                res.put("msg","用户注册成功"); // 提示信息信息
            } else {
                // 已注册
                User bindUser = userService.bindWechat(userId, openId, nickname, header);
                res.put("user", bindUser);
                res.put("flag","bind");
                res.put("msg","微信绑定成功");
            }
        } else {
            res.put("user", user);
            res.put("flag","login");
            res.put("msg","用户登录成功");
        }

        // 将结果序列化为json字符串
        ObjectMapper objectMapper = new ObjectMapper();
        String resJson = objectMapper.writeValueAsString(res);
        String encodedRes = URLEncoder.encode(resJson, StandardCharsets.UTF_8);

        // 将结果放入cookie中
        Cookie resCookie = new Cookie("res", encodedRes);
        resCookie.setPath("/");

        // 将cookie放入响应中
        response.addCookie(resCookie);

        // 重定向到前端页面
        String redirectUrl = "your-front-end-page";
        response.setStatus(HttpServletResponse.SC_FOUND);
        response.setHeader("Location", redirectUrl);
    }

    //查询是否绑定微信
    @GetMapping("/wechat/isBind")
    public ApiResponse<Boolean> isBind(@RequestParam String id) {
        User user = userService.getUserById(id);
        if (Objects.isNull(user)) {
            return ApiResponse.error(ResponseCodeEnum.NOT_FOUND.getCode(), "用户不存在", false);
        }
        String openId = user.getOpenId();
        if (openId == null || openId.isEmpty()) {
            return ApiResponse.success("未绑定微信", false);
        }
        return ApiResponse.success("已绑定微信", true);
    }

}
  1. ServiceImpl:

给出微信绑定, 获取access_token和获取微信个人信息的简单逻辑以供参考

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Value("${wechat.appid}")
    private String appId;

    @Value("${wechat.secret}")
    private String appSecret;

    @Override
    public User bindWechat(String id, String openId, String nickname, String header) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new IllegalArgumentException("用户不存在");
        }

        if (!Objects.isNull(user.getOpenId())) {
            throw new IllegalArgumentException("微信已绑定,请勿重复操作");
        }

        user.setOpenId(openId);
        user.setWechatNickname(nickname);
        user.setWechatHeader(header);
        int updateCount = userMapper.updateById(user);
        if (updateCount != 1) {
            throw new RuntimeException("绑定微信失败");
        }
        return user;
    }

    @Override
    public Map<String, Object> getAccessToken(String code) {
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                "?appid=" + appId +
                "&secret=" + appSecret +
                "&code=" + code +
                "&grant_type=authorization_code";
        return restTemplate.getForObject(url, Map.class);
    }

    @Override
    public Map<String, Object> getUserInfo(String accessToken, String openId) {
        String url = "https://api.weixin.qq.com/sns/userinfo" +
                "?access_token=" + accessToken +
                "&openid=" + openId +
                "&lang=zh_CN";
        return restTemplate.getForObject(url, Map.class);
    }
}

4.总结

微信登录的全流程总结如下:

  1. 前端将登录二维码嵌入到自己的web页面中
  2. 用户扫描二维码, 进行微信登录
  3. 扫码成功后, 重定向到开发者自定义的接口链接(即redirectUri), 按照指定的逻辑进行处理, 这一步中会自动将生成的code作为接口的参数进行传入
  4. 进入登录逻辑, 首先根据code去换取access_token, 然后根据access_token去获取用户信息, 从用户信息中可以提取出用户的昵称和头像, 以供后续使用
  5. 接口的返回值可以是一个链接, 即是微信登录成功后, 想要跳转到的页面, 同时其他需要返回的参数可以放入cookie中一并返回给前端