vue2+springboot实现微信扫码登录功能

874 阅读7分钟

本文前端主要采用 wxlogin 内嵌二维码.(需要先到链接: 微信平台申请,申请后会生成一个id和secret)

前言

目前用户使用微信登录第三方应用或网站,无非两种展现方式

  • 第一种, 网页外链跳转方式
  • 第二种, 网页内嵌二维码方式
  1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
  2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
  3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。 参数说明
参数是否必须说明
self_redirecttrue:手机点击确认登录后可以在 iframe 内跳转到 redirect_uri,false:手机点击确认登录后可以在 top window 跳转到 redirect_uri。默认为 false。
id第三方页面显示二维码的容器id
appid应用唯一标识,在微信开放平台提交应用审核通过后获得
scope应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
redirect_uri重定向地址,需要进行UrlEncode
state用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验
style提供"black"、"white"可选,默认为黑色文字描述。详见文档底部FAQ
href自定义样式链接,第三方可根据实际需求覆盖默认样式。详见文档底部FAQ

在这里主要说的是第二种,将微信二维码内嵌到网站页面中,在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。

这里使用vue-wxlogin,方便组件化模块化引入

//安装vue-wxlogin
npm install vue-wxlogin --save-dev


//引入页面
import wxlogin from 'vue-wxlogin';
// 注册组件
components: {
  wxlogin,
},

前端代码:在扫码之后网页会跳转,在created获取路由的信息,拿到路由中的code信息,然后请求后台,完成登录。

<template>
        <h2>请扫码登录</h2>
        <wxlogin
            class="login"
            :appid= appid
            :scope="'snsapi_login'"
            :theme="'black'"
            :redirect_uri='encodeURIComponent(redirect_url)'
            :href="'data:text/css;base64,LmltcG93ZXJCb3ggLnRpdGxlIHsKICBkaXNwbGF5OiBub25lOwp9Ci5pbXBvd2VyQm94IC5xcmNvZGUgewogIHdpZHRoOiAyMjVweDsKICBoZWlnaHQ6IDIyNXB4Owp9'"
            :state="`${Math.random()}`"
            :delay="5000"
        >
        </wxlogin>
</template>

<script>
import wxlogin from 'vue-wxlogin';
import {ElMessage} from "element-plus";

export default {
  name: "loginView",
  data() {
    return {
      appid: '',
      redirect_url:'',
    };
  },
  // 注册组件
  components: {
    wxlogin,
  },
  mounted() {
    //获取微信appid
    this.wxobtain();
  },
  created() {
    if (this.$route.redirectedFrom) {
      if (this.$route.redirectedFrom.query.code) {
        let openid = this.$route.redirectedFrom.query.code
        this.$axios.get('/userInfo/callback/' + openid).then((response) => {
          if (response.code === 200) {
            // 存储token开始时间
            window.localStorage.setItem('tokenStartTime', new Date().getTime())
            window.localStorage.setItem('token', response)
            this.$router.push("/trackLive");
            ElMessage({
              message: "登录成功",
              type: "success",
            });
          } else if (response.code === 201) {
            ElMessage({
              message: "不存在改用户",
              type: "error",
            });
          } else {
            this.$message({
              message: response.body.msg,
              type: 'error'
            })
          }
        })
      }
    }
  },
  methods: {
    wxobtain(){
      this.$axios
          .get("/userInfo/wxlogin")
          .then((response) => {
            //获取appid
            const regex = /appid=([^&]+)/;
            const match = response.match(regex);
            //获取扫码后跳转的地址
            const url = new URL(response);
            this.redirect_url = decodeURIComponent(url.searchParams.get('redirect_uri'));

            if (match) {
              this.appid = match[1];
            } else {
              console.log('Appid not found');
            }
          });
    }
  },
};
</script>

<style src="../style/login_style.css" scoped>
</style>

yml配置:微信登录所需信息:这个redirect_url 这个端口在微信开发平台设定好。我这里定义的是前端端口

wx:
  open:
    app_id: wxed8945sdfwe**
    app_secret: a748251723517*****8**
    redirect_url: http://127.0.0.1:8080/trackLive

引入

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ConstantWxPropertiesUtils implements InitializingBean {

    @Value("${wx.open.app_id}")
    private String appId;

    @Value("${wx.open.app_secret}")
    private String appSecret;

    @Value("${wx.open.redirect_url}")
    private String redirectUrl;


    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;


    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;

    }
}

后端代码:方法中生成jwt的方法可以注释掉

 //一、扫码登录
    @GetMapping("wxlogin")
    public String getWxCode() {
        //微信开放平台baseurl,%s相当于?占位符
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s#wechat_redirect";
        String redirectUri = null;
        try {
            redirectUri = URLEncoder.encode(ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        //设置%s里面的值
        String url = String.format(baseUrl, ConstantWxPropertiesUtils.WX_OPEN_APP_ID, redirectUri, "hjh520");

        //重定向到微信地址里面
        return url;
    }

    //二、验证扫码登录人员信息
    @GetMapping("callback/{code}")
    public R callback(@PathVariable(value = "code") String code) {
        try {
            //baseAccessTokenUrl,%s相当于?占位符
            String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                    "?appid=%s" +
                    "&secret=%s" +
                    "&code=%s" +
                    "&grant_type=authorization_code";
            //设置%s里面的值
            String url = String.format(baseAccessTokenUrl, ConstantWxPropertiesUtils.WX_OPEN_APP_ID, ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET, code);
            //使用httpClient发送请求,得到返回值
            String accessTokenInfo = HttpUtils.get(url);
            //解析用户信息
            JSONObject userInfoJson = JSONObject.parseObject(accessTokenInfo);

            //使用gson将字符串转换成对象
            Gson gson = new Gson();
            HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
            String accessToken = (String) mapAccessToken.get("access_token");
            String openid = (String) mapAccessToken.get("openid");

            //访问微信的资源服务器,获取用户信息
            String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                    "?access_token=%s" +
                    "&openid=%s";
            //拼接两个参数
            String userInfoUrl = String.format(
                    baseUserInfoUrl,
                    accessToken,
                    openid);
            //发送请求
            String userInfo = HttpUtils.get(userInfoUrl);

            //获取返回userInfo字符串扫描人信息
            HashMap<String, String> userInfoMap = gson.fromJson(userInfo, HashMap.class);
            String nickname = userInfoMap.get("nickname");
//            String headimgurl = userInfoMap.get("headimgurl");


            //判断数据表里面是否存在相同我微信信息,根据openid判断
            UserInfo ucenterMember = userInfoService.getMemberByOponId(openid);
            if (ucenterMember == null) {
                UserInfo user = new UserInfo();
                user.setUserName("outsider");

                List<UserInfo> userInfoList = userInfoService.queryByUser(user);
                user.setOpenid(openid);
                user.setPassword("NO-NEED");
                user.setWeChatName(nickname);
                user.setLevel(0);
                //防止外人一直扫码登录,限制保留部分人的信息
                if (userInfoList.size() > 10) {
                    return R.fail();
                }
                userInfoService.insert(user);
                return R.fail();
            }

            //使用JWT生成token
            Map<String, Object> claims = new HashMap<>();
            claims.put(JwtClaimsConstant.USER_ID, "mtl");
            String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),
                    jwtProperties.getAdminTtl(),
                    claims);
            //最后:返回首页
            //也可以返回token前端自己去跳转页面
            return R.ok(token);
        } catch (Exception e) {

        }
        return R.fail();
    }

http工具类:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;

/**
 * HttpClient工具类
 *
 * @author zyf
 * @since 2024-01-10
 */
public class HttpUtils {

    /**
     * 字符编码
     */
    private final static String UTF8 = "utf-8";
    /**
     * 字节流数组大小(1MB)
     */
    private final static int BYTE_ARRAY_LENGTH = 1024 * 1024;

    /**
     * 执行get请求获取响应
     *
     * @param url 请求地址
     * @return 响应内容
     */
    public static String get(String url) {
        return get(url, null);
    }

    /**
     * 执行get请求获取响应
     *
     * @param url     请求地址
     * @param headers 请求头参数
     * @return 响应内容
     */
    public static String get(String url, Map<String, String> headers) {
        HttpGet get = new HttpGet(url);
        return getRespString(get, headers);
    }

    /**
     * 执行post请求获取响应
     *
     * @param url 请求地址
     * @return 响应内容
     */
    public static String post(String url) {
        return post(url, null, null);
    }

    /**
     * 执行post请求获取响应
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return 响应内容
     */
    public static String post(String url, Map<String, String> params) {
        return post(url, null, params);
    }

    /**
     * 执行post请求获取响应
     *
     * @param url     请求地址
     * @param headers 请求头参数
     * @param params  请求参数
     * @return 响应内容
     */
    public static String post(String url, Map<String, String> headers, Map<String, String> params) {
        HttpPost post = new HttpPost(url);
        post.setEntity(getHttpEntity(params));
        return getRespString(post, headers);
    }

    /**
     * 执行post请求获取响应(请求体为JOSN数据)
     *
     * @param url  请求地址
     * @param json 请求的JSON数据
     * @return 响应内容
     */
    public static String postJson(String url, String json) {
        return postJson(url, null, json);
    }

    /**
     * 执行post请求获取响应(请求体为JOSN数据)
     *
     * @param url     请求地址
     * @param headers 请求头参数
     * @param json    请求的JSON数据
     * @return 响应内容
     */
    public static String postJson(String url, Map<String, String> headers, String json) {
        HttpPost post = new HttpPost(url);
        post.setHeader("Content-type", "application/json");
        post.setEntity(new StringEntity(json, UTF8));
        return getRespString(post, headers);
    }


    /**
     * 下载文件
     *
     * @param url      下载地址
     * @param path     保存路径(如:D:/images,不传默认当前工程根目录)
     * @param fileName 文件名称(如:hello.jpg)
     */
    public static void download(String url, String path, String fileName) {
        HttpGet get = new HttpGet(url);
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        String filePath = null;
        if (Objects.isNull(path) || path.isEmpty()) {
            filePath = fileName;
        } else {
            if (path.endsWith("/")) {
                filePath = path + fileName;
            } else {
                filePath += path + "/" + fileName;
            }
        }
        File file = new File(filePath);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try (FileOutputStream fos = new FileOutputStream(file); InputStream in = getRespInputStream(get, null)) {
            if (Objects.isNull(in)) {
                return;
            }
            byte[] bytes = new byte[BYTE_ARRAY_LENGTH];
            int len = 0;
            while ((len = in.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取请求体HttpEntity
     *
     * @param params 请求参数
     * @return HttpEntity
     */
    private static HttpEntity getHttpEntity(Map<String, String> params) {
        List<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
        for (Entry<String, String> entry : params.entrySet()) {
            pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }
        HttpEntity entity = null;
        try {
            entity = new UrlEncodedFormEntity(pairs, UTF8);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return entity;
    }

    /**
     * 设置请求头
     *
     * @param request 请求对象
     * @param headers 请求头参数
     */
    private static void setHeaders(HttpUriRequest request, Map<String, String> headers) {
        if (Objects.nonNull(headers) && !headers.isEmpty()) {
            // 请求头不为空,则设置对应请求头
            for (Entry<String, String> entry : headers.entrySet()) {
                request.setHeader(entry.getKey(), entry.getValue());
            }
        } else {
            // 请求为空时,设置默认请求头
            request.setHeader("Connection", "keep-alive");
            request.setHeader("Accept-Encoding", "gzip, deflate, br");
            request.setHeader("Accept", "*/*");
            request.setHeader("User-Agent",
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36");
        }
    }

    /**
     * 执行请求,获取响应流
     *
     * @param request 请求对象
     * @return 响应内容
     */
    private static InputStream getRespInputStream(HttpUriRequest request, Map<String, String> headers) {
        // 设置请求头
        setHeaders(request, headers);
        // 获取响应对象
        HttpResponse response = null;
        try {
            response = HttpClients.createDefault().execute(request);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        // 获取Entity对象
        HttpEntity entity = response.getEntity();
        // 获取响应信息流
        InputStream in = null;
        if (Objects.nonNull(entity)) {
            try {
                in = entity.getContent();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return in;
    }

    /**
     * 执行请求,获取响应内容
     *
     * @param request 请求对象
     * @return 响应内容
     */
    private static String getRespString(HttpUriRequest request, Map<String, String> headers) {
        byte[] bytes = new byte[BYTE_ARRAY_LENGTH];
        int len = 0;
        try (InputStream in = getRespInputStream(request, headers);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            if (Objects.isNull(in)) {
                return "";
            }
            while ((len = in.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            return bos.toString(UTF8);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

}

maven引入:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

<!-- HttpClinet 核心包 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

最后请求成功,本人亲测。