vue3 + springboot实现微信登录

创建VUE3项目

创建初始文件

进入项目存放位置

右键用命令行打开(终端打开)

复制代码
 npm create vite@latest wechat-report --template vue

npm:包管理需要安装node.js

Vite:用于热部署和生成、打包项目

--template vue:模板指定为vue

可能报错: 因为在此系统上禁止运行脚本。有关详细信息

会提示是否继续:

Ok to proceed? (y)

输入y回车

会让选择版本和类型:

如:◆ Select a framework:

│ ○ Vanilla

│ ● Vue

│ ○ React

用方向键移动到VUE,回车

用方向键移动到JavaScript ,回车

移动到创建好的项目中初始化

复制代码
cd wechat-report
npm install

安装Element Plus

复制代码
# 安装Element Plus
npm install element-plus @element-plus/icons-vue

# 安装自动导入插件(可选但推荐)
npm install -D unplugin-vue-components unplugin-auto-import

第一行安装elementPlus 和图标库

第二行是自动导入插件:自动按需导入 Vue 组件(如 Element Plus 的 <el-button>),无需手动 import

  • unplugin-auto-import:自动导入 Vue 相关的 API(如 ref、reactive、onMounted 等),减少手动导入的代码。
  • 减少代码量 :不用手动写 import { ElButton } from 'element-plus'。
  • 优化打包体积 :只打包实际用到的组件,避免全量引入。

配置项目

进入项目,找到

修改vite.config.js:

设置elementui,@/路径功能,后台地址映射

复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)) // 使用现代ESM方式
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://your-backend-api.com', // 替换为实际后端地址
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
})

安装微信JSSDK

复制代码
npm install weixin-js-sdk

创建API服务

在src/api目录下创建wechat.js:

复制代码
import axios from 'axios'

const service = axios.create({
  baseURL: '/api', // 根据实际后端API地址配置
  timeout: 5000
})

// 获取校验url,用于登录
export function getAuthorizationUrl(url) {
    return service({
        method: 'post',
        url: '/api/emergency/wxLoginReport/getAuthorizationUrl',
        data: { url }
    })
}

// 其他API...

修改App.vue

清空内容

复制代码
<template>
  <router-view />
</template>

<script>
export default {
  name: 'App',
  setup() {
    return {}
  }
}
</script>

<style>
/* 全局样式 */
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  max-width: 100vw;
  min-height: 100vh;
  background-color: #f5f5f5;
}
</style>

配置路由

安装Vue Router:

复制代码
npm install vue-router@4

创建src/router/index.js:

创建登录页面和结果页面

复制代码
import { createRouter, createWebHashHistory } from 'vue-router'

const routes = [
    //登录页面
    {
        path: '/',
        name: 'Home',
        component: () => import('@/views/Home.vue')
    },
    //登录结果
    {
        path: '/Home2',
        name: 'Home2',
        component: () => import('@/views/Home2.vue')
    },
    // 其他路由...
]

const router = createRouter({
    history: createWebHashHistory(), // 使用hash模式,兼容微信公众号
    routes
})

export default router

创建示例页面

创建src/views/Home.vue:

用于测试微信功能登录

复制代码
<template>
  <div class="home">
    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>微信公众号示例</span>
        </div>
      </template>
      <el-button @click="loginAuth">登录</el-button>
    </el-card>
  </div>
</template>

<script>
import {getAuthorizationUrl} from '@/api/wechat'

export default {
  name: 'Home',
  setup() {

    const loginAuth = () => {
      // let url = window.location.href;
      let url = 'http://192.168.10.213:5173/#/Home2';
      // console.log(url)
      getAuthorizationUrl(url).then(res => {
        console.log(res.data.msg)
        window.location.href = res.data.msg;
      })
    };
    return {
      loginAuth
    }
  }
}
</script>

<style scoped>
.home {
  padding: 20px;
}

.box-card {
  max-width: 500px;
  margin: 0 auto;
}

.card-header {
  font-size: 18px;
  font-weight: bold;
}

</style>

创建src/views/Home2.vue:

登录结果页面

复制代码
<template>
  <div class="wechat-auth-result">
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>

    <!-- 成功状态 -->
    <div v-if="userInfo && !error" class="success">
      <div class="header">
        <h2>微信登录成功</h2>
        <p>{{ userInfo.message }}</p>
      </div>

      <div class="profile">
        <img :src="userInfo.headimgurl" alt="用户头像" class="avatar">
        <div class="details">
          <h3>{{ userInfo.nickname }}</h3>
<!--          <button @click="goToDashboard" class="btn btn-primary">进入首页</button>-->
        </div>
      </div>
    </div>

    <!-- 错误状态 -->
    <div v-if="error" class="error">
      <h2>登录失败</h2>
      <p>{{ error }}</p>
      <button @click="retryLogin" class="btn btn-secondary">重新登录</button>
    </div>
  </div>
</template>

<script>
import {ref, onMounted} from 'vue';
import {useRoute, useRouter} from 'vue-router';

export default {
  setup() {
    const route = useRoute();
    const router = useRouter();
    const userInfo = ref(null);
    const loading = ref(true);
    const error = ref(null);

    // 获取并处理URL参数
    const parseUserInfo = () => {
      try {
        if (route.query.openid) {
          userInfo.value = {
            openid: route.query.openid,
            nickname: decodeURIComponent(route.query.nickname || '未知用户'),
            headimgurl: decodeURIComponent(route.query.headimgurl || ''),
            message: decodeURIComponent(route.query.msg || '登录成功')
          };

          // 清理URL
          window.history.replaceState({}, '', window.location.pathname);

          // 这里可以添加将用户信息存储到Vuex/Pinia或发送到后端验证的逻辑
        } else {
          error.value = '未获取到用户信息';
        }
      } catch (e) {
        error.value = '解析用户信息失败: ' + e.message;
      } finally {
        loading.value = false;
      }
    };

    // const goToDashboard = () => {
    //   router.push('/dashboard');
    // };

    const retryLogin = () => {
      router.push('/');
    };

    onMounted(() => {
      parseUserInfo();
    });

    return {userInfo, loading, error,  retryLogin};
  }
};
</script>

<style scoped>
.wechat-auth-result {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.loading {
  text-align: center;
  padding: 40px 0;
}

.success .header {
  text-align: center;
  margin-bottom: 30px;
}

.profile {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-top: 20px;
}

.avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
}

.details {
  flex: 1;
}

.error {
  color: #dc3545;
  text-align: center;
  padding: 40px 0;
}

.btn {
  margin-top: 15px;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary {
  background-color: #007bff;
  color: white;
  border: none;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
  border: none;
}
</style>

修改main.js

复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

// 引入Element Plus样式
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(router)
app.mount('#app')

package.json

加上--host,用与显示内容

复制代码
"scripts": {
  "dev": "vite --host"
}

npm run dev 启动

springboot后台

controller层

用于拼接微信调用地址与处理回调信息

复制代码
package yunline.controller;

import yunline.entity.TdSspUserEntity;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import yunline.base.ActionResult;
import yunline.service.TdSspUserService;
import yunline.utils.WxReportUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Api(tags = "应急排班系统", value = "emergency")
@RestController
@RequestMapping("/api/emergency/wxLoginReport")
public class WxReportController {
    @Autowired
    private TdSspUserService tdSspUserService;

    @ApiOperation("获取微信授权地址")
    @PostMapping("/getAuthorizationUrl")
    public ActionResult getAuthorizationUrl(@RequestBody Map<String, String> params) throws Exception {
        System.out.println("进入");
        String url = params.get("url");
        url = URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
        return ActionResult.success(WxReportUtils.getAuthorizationUrl(url));
    }


    /**
     * 当用户授权后,微信会重定向到你指定的URI,并携带一个code参数。你需要捕获这个请求并提取code。
     */
    @ApiOperation("捕获微信授权回执")
    @GetMapping("/handleAuthorizationCallback")
    public void handleAuthorizationCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        Map<String, String[]> c = request.getParameterMap();

        if (code == null) {
            // 处理错误情况
//            response.getWriter().println("授权失败,请重试!");
//            return ActionResult.fail("授权失败,请重试!");

            String frontendUrl = URLDecoder.decode(state, "UTF-8")
                    +"?openid="
                    +"&msg=授权失败!";
            response.sendRedirect(frontendUrl);
            return ;
        }
        // 获取access_token和openid
        Map<String, String> tokenInfo = WxReportUtils.getTokenInfo(code);

        if (tokenInfo == null || !tokenInfo.containsKey("access_token") || !tokenInfo.containsKey("openid")) {
//            response.getWriter().println("获取access_token失败,请重试!");
//            return ActionResult.fail("获取access_token失败,请重试!");

            String frontendUrl = URLDecoder.decode(state, "UTF-8")
                    +"?openid="
                    +"&msg=获取access_token失败!";
            response.sendRedirect(frontendUrl);
            return ;
        }

        String accessToken = tokenInfo.get("access_token");
        String openid = tokenInfo.get("openid");

        // 获取用户信息
        Map<String, Object> userInfo = WxReportUtils.getUserInfo(accessToken, openid);

        if (userInfo == null || !userInfo.containsKey("nickname") || !userInfo.containsKey("headimgurl")) {
//            response.getWriter().println("获取用户信息失败,请重试!");
//            return ActionResult.fail("获取用户信息失败,请重试!");

            String frontendUrl = URLDecoder.decode(state, "UTF-8")
                    +"?openid="
                    +"&msg=获取用户信息失败!";
            response.sendRedirect(frontendUrl);
            return ;
        }

        // 完成本地用户认证
        String nickname = (String) userInfo.get("nickname");
        String headimgurl = (String) userInfo.get("headimgurl");
//        String sex = (String) userInfo.get("sex");
        Map<String, Object> map = new HashMap<>();
        int has = tdSspUserService.lambdaQuery().eq(TdSspUserEntity::getRemark, openid).count();
        if (has > 0) {
            //修改
            tdSspUserService.lambdaUpdate().eq(TdSspUserEntity::getRemark, openid)
                    .set(TdSspUserEntity::getPhotopath,headimgurl)
                    .set(TdSspUserEntity::getUserId,nickname).update();
        }else{
            //创建
            TdSspUserEntity entity = new TdSspUserEntity();
            entity.setUserId(nickname);
            entity.setPhotopath(headimgurl);
            entity.setRemark(openid);
            entity.setNickName(nickname);
            entity.setUserRegdate(new Date());
            tdSspUserService.create(entity);
        }
        map.put("userInfo", userInfo);
        map.put("openid", openid);
//        return ActionResult.success(map);
        //重定向
        String frontendUrl = URLDecoder.decode(state, "UTF-8")
                +"?openid=" + openid
                +"&msg=登录成功"
                +"&nickname="+nickname
                +"&headimgurl="+headimgurl
                ;
        response.sendRedirect(frontendUrl);

//        return null;
    }

}

创建WxReportUtils,用于对接微信

复制代码
package yunline.utils;

import cn.hutool.json.JSONUtil;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class WxReportUtils {
    private static final String IP = getIP();
    private static final String APPID = "你的appid";
    private static final String APPSECRET = "你的APPSECRET";
//    private static final String SCOPE = "snsapi_base";
//    private static final String SCOPE = "snsapi_login";
    private static final String SCOPE = "snsapi_userinfo";
    private static final String PORT = "28888";
    private static final String STATE = "STATE";

    private static final String REDIRECT_URI = "后台调用地址,就是controller层的callback";

    private static String getIP() {
        try {
            InetAddress localhost = InetAddress.getLocalHost();
//            IP=localhost.getHostAddress();
            return localhost.getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static String getAuthorizationUrl() throws Exception {
        return getAuthorizationUrl(STATE);
    }
    public static String getAuthorizationUrl(String url) throws Exception {
        return "https://open.weixin.qq.com/connect/oauth2/authorize?"
                + "appid=" + APPID
                + "&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8.toString())
                + "&response_type=code"
                + "&scope=" + SCOPE
//                + "&state=" + STATE
                + "&state=" + url
                + "#wechat_redirect";
    }


    /**
     * 获取token和openid
     *
     * @param code
     * @return
     * @throws Exception
     */
    public static Map<String, String> getTokenInfo(String code) throws Exception {
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?"
                + "appid=" + APPID
                + "&secret=" + APPSECRET
                + "&code=" + code
                + "&grant_type=authorization_code";

        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");

        int responseCode = connection.getResponseCode();
        if (responseCode == 200) {
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder content = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                content.append(inputLine);
            }
            in.close();
            connection.disconnect();
            return JSONUtil.toBean(content.toString(), Map.class, false);
        } else {
            throw new RuntimeException("请求失败:" + responseCode);
        }
    }

    /**
     * 获取用户信息
     *
     * @param accessToken
     * @param openId
     * @return
     * @throws Exception
     */
    public static Map<String, Object> getUserInfo(String accessToken, String openId) throws Exception {
        String url = "https://api.weixin.qq.com/sns/userinfo?"
                + "access_token=" + accessToken
                + "&openid=" + openId
                + "&lang=zh_CN";

        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");

        int responseCode = connection.getResponseCode();
        if (responseCode == 200) {
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder content = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                content.append(inputLine);
            }
            in.close();
            connection.disconnect();

            return JSONUtil.toBean(content.toString(), Map.class, false);
        } else {
            throw new RuntimeException("请求失败:" + responseCode);
        }
    }
}

微信配置

进入微信测试地址:

https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo

注册后进行配置

配置获取用户信息的权限,配置一下域名

内网穿透(可选

这里我使用的是花生壳网穿透获得的外网地址,用了10块钱巨款

结果演示

相关推荐
vivo互联网技术2 小时前
Spring Boot 启动优化实践
后端·springboot·spring启动加速·异步初始化
海的诗篇_1 天前
前端开发面试题总结-vue2框架篇(三)
前端·javascript·css·面试·vue·html
LUCIAZZZ3 天前
项目拓展-Apache对象池,对象池思想结合ThreadLocal复用日志对象
java·jvm·数据库·spring·apache·springboot
ZKf30FkG3 天前
理解 package.json 中的版本控制:“nuxt“: “3.16.0“ vs “nuxt“: “^3.16.0“ 的深层差异
vue
海的诗篇_3 天前
前端开发面试题总结-vue2框架篇(二)
前端·javascript·css·vue.js·前端框架·vue
LUCIAZZZ4 天前
钉钉机器人-自定义卡片推送快速入门
java·jvm·spring boot·机器人·钉钉·springboot
LUCIAZZZ4 天前
项目拓展-Jol分析本地对象or缓存的内存占用
java·开发语言·jvm·数据库·缓存·springboot
雨果talk4 天前
【一文看懂多模块Bean初始化难题】Spring Boot多模块项目中的Bean初始化难题:包名不一致的优雅解决方案
java·spring boot·后端·spring·springboot