微信登录模块封装

文章目录

1.资质申请

  1. 主体为企业的域名和备案的服务器
  2. 主体为企业的微信开放平台的开发者资质认证
  3. 微信开放平台创建应用获取AppID和AppSecret

2.combinations-wx-login-starter

1.目录结构
2.pom.xml 引入okhttp依赖
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.sunxiansheng</groupId>
        <artifactId>sunrays-combinations</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>combinations-wx-login-starter</artifactId>
    <!-- 项目名 -->
    <name>${project.groupId}:${project.artifactId}</name>
    <!-- 简单描述 -->
    <description>微信登录模块封装</description>

    <dependencies>
        <!-- okhttp -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>
    </dependencies>
</project>
3.WxLoginProperties.java 属性配置
java 复制代码
package cn.sunxiansheng.wx.login.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Description: 微信登录的属性配置
 *
 * @Author sun
 * @Create 2025/1/23 18:49
 * @Version 1.0
 */
@ConfigurationProperties(prefix = "sun-rays.wx.login")
@Data
public class WxLoginProperties {

    /**
     * 微信开放平台应用的AppID
     */
    private String appId;

    /**
     * 微信开放平台应用的AppSecret
     */
    private String appSecret;

    /**
     * 微信开放平台的access_token_url前缀,有默认值,可以不填,为了防止变化!
     */
    private String accessTokenUrlPrefix = "https://api.weixin.qq.com/sns/oauth2/access_token";
}
4.WxLoginUtil.java 后端通过 code 获取 access_token的工具类
java 复制代码
package cn.sunxiansheng.wx.login.utils;

import cn.sunxiansheng.wx.login.config.properties.WxLoginProperties;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import javax.annotation.Resource;

/**
 * Description: 微信登录工具类
 *
 * @Author sun
 * @Create 2025/1/23 19:01
 * @Version 1.0
 */
@Slf4j
public class WxLoginUtil {

    /**
     * 获取微信登录的配置
     */
    @Resource
    private WxLoginProperties wxLoginProperties;

    /**
     * 微信登录的响应类
     */
    @Data
    public static class AccessTokenResponse {

        @SerializedName("access_token")
        private String accessToken;

        @SerializedName("expires_in")
        private Integer expiresIn;

        @SerializedName("refresh_token")
        private String refreshToken;

        @SerializedName("openid")
        private String openId;

        @SerializedName("scope")
        private String scope;

        @SerializedName("unionid")
        private String unionId;
    }

    /**
     * 根据code来完成微信登录
     *
     * @param code 微信开放平台返回的code
     * @return 返回AccessTokenResponse
     */
    public AccessTokenResponse wxLogin(String code) {
        return getAccessToken(wxLoginProperties.getAppId(), wxLoginProperties.getAppSecret(), code);
    }

    /**
     * 后端通过 code 获取 access_token
     *
     * @param appid  微信应用的 appid
     * @param secret 微信应用的 secret
     * @param code   后端已经获得的 code 参数
     * @return 返回封装的 AccessTokenResponse 对象
     */
    private AccessTokenResponse getAccessToken(String appid, String secret, String code) {
        // 构造请求 URL
        String url = String.format("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
                wxLoginProperties.getAccessTokenUrlPrefix(), appid, secret, code);

        // 创建 OkHttpClient 实例
        OkHttpClient client = new OkHttpClient();

        // 创建 Request 对象
        Request request = new Request.Builder()
                .url(url)
                .build();

        // 执行请求并处理响应
        try (Response response = client.newCall(request).execute()) {
            // 检查请求是否成功
            if (!response.isSuccessful()) {
                String responseBody = response.body() != null ? response.body().string() : "响应体为空";
                log.error("后端通过 code 获取 access_token 的请求失败,响应码:{}, 响应体:{}", response.code(), responseBody);
                return null;
            }

            // 打印成功的响应
            String jsonResponse = response.body() != null ? response.body().string() : "响应体为空";
            log.info("成功获取 access_token,响应:{}", jsonResponse);

            // 使用 Gson 解析 JSON 数据并封装成 AccessTokenResponse 对象
            Gson gson = new Gson();

            // 返回封装的对象
            return gson.fromJson(jsonResponse, AccessTokenResponse.class);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            // 返回 null 或者其他错误处理
            return null;
        }
    }
}
5.WxLoginAutoConfiguration.java 自动配置类
java 复制代码
package cn.sunxiansheng.wx.login.config;

import cn.sunxiansheng.wx.login.config.properties.WxLoginProperties;
import cn.sunxiansheng.wx.login.utils.WxLoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

/**
 * Description: 微信登录自动配置类
 *
 * @Author sun
 * @Create 2025/1/13 16:11
 * @Version 1.0
 */
@Configuration
@EnableConfigurationProperties({WxLoginProperties.class})
@Slf4j
public class WxLoginAutoConfiguration {

    /**
     * 自动配置成功日志
     */
    @PostConstruct
    public void logConfigSuccess() {
        log.info("WxLoginAutoConfiguration has been loaded successfully!");
    }

    /**
     * 注入WxLoginUtil
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    WxLoginUtil wxLoginUtil() {
        return new WxLoginUtil();
    }
}
6.spring.factories 激活自动配置类
properties 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.sunxiansheng.wx.login.config.WxLoginAutoConfiguration

3.combinations-wx-starter-demo

1.目录结构
2.pom.xml 引入依赖
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.sunxiansheng</groupId>
        <artifactId>sunrays-combinations-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>combinations-wx-starter-demo</artifactId>

    <dependencies>
        <!-- combinations-wx-login-starter -->
        <dependency>
            <groupId>cn.sunxiansheng</groupId>
            <artifactId>combinations-wx-login-starter</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- common-web-starter -->
        <dependency>
            <groupId>cn.sunxiansheng</groupId>
            <artifactId>common-web-starter</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <!-- maven 打包常规配置 -->
    <build>
        <!-- 打包成 jar 包时的名字为项目的artifactId + version -->
        <finalName>${project.artifactId}-${project.version}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <!-- 引用父模块中统一管理的插件版本(与SpringBoot的版本一致! -->
                <executions>
                    <execution>
                        <goals>
                            <!-- 将所有的依赖包都打到这个模块中 -->
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
3.application.yml 配置AppID和AppSecret
yaml 复制代码
sun-rays:
  log4j2:
    home: /Users/sunxiansheng/IdeaProjects/sunrays-framework/sunrays-combinations-demo/combinations-wx-starter-demo/logs # 日志存储根目录
  env:
    path: /Users/sunxiansheng/IdeaProjects/sunrays-framework/sunrays-combinations-demo/combinations-wx-starter-demo # .env文件的绝对路径
  wx:
    login:
      app-id: ${WX_LOGIN_APP_ID} # 微信开放平台应用的AppID
      app-secret: ${WX_LOGIN_APP_SECRET} # 微信开放平台应用的AppSecret
spring:
  profiles:
    active: prod # 激活的环境
4.application-prod.yml 配置生产环境的日志和.env文件路径
yaml 复制代码
sun-rays:
  log4j2:
    home: /www/wwwroot/sunrays-framework/logs # 日志存储根目录
  env:
    path: /www/wwwroot/sunrays-framework # .env文件的绝对路径
5.CodeAndState.java 接受code和state的bean
java 复制代码
package cn.sunxiansheng.wx.entity;

import lombok.Data;

/**
 * Description: 接受code和state的bean
 *
 * @Author sun
 * @Create 2025/1/16 19:15
 * @Version 1.0
 */

@Data
public class CodeAndState {
    /**
     * 微信的code
     */
    private String code;
    /**
     * 微信的state
     */
    private String state;
}
6.WxLoginController.java 微信登录Controller
java 复制代码
package cn.sunxiansheng.wx.controller;

import cn.sunxiansheng.wx.entity.CodeAndState;
import cn.sunxiansheng.wx.login.utils.WxLoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Description: 微信登录Controller
 *
 * @Author sun
 * @Create 2025/1/13 16:26
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("/wx")
public class WxLoginController {

    @Resource
    private WxLoginUtil wxLoginUtil;

    @RequestMapping("/test")
    public String test() {
        return "test";
    }

    /**
     * 微信登录
     *
     * @param codeAndState 前端传过来的code和state
     * @return 返回unionId
     */
    @RequestMapping("/login")
    public String login(@RequestBody CodeAndState codeAndState) {
        // 使用code来完成微信登录
        WxLoginUtil.AccessTokenResponse accessTokenResponse = wxLoginUtil.wxLogin(codeAndState.getCode());
        if (accessTokenResponse == null) {
            log.error("accessToken is null");
            return "null";
        }
        // 获取unionId
        String unionId = accessTokenResponse.getUnionId();
        if (unionId == null) {
            log.error("unionId is null");
            return "null";
        }
        // 获取unionId
        return accessTokenResponse.getUnionId();
    }
}
7.WxApplication.java 启动类
java 复制代码
package cn.sunxiansheng.wx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Description: 微信启动类
 *
 * @Author sun
 * @Create 2025/1/13 17:54
 * @Version 1.0
 */
@SpringBootApplication
public class WxApplication {

    public static void main(String[] args) {
        SpringApplication.run(WxApplication.class, args);
    }
}

4.微信登录流程梳理

1.用户点击微信登录按钮
2.前端向开放平台发送请求主要携带appId和redirectUri
vue 复制代码
<template>
  <button @click="handleLogin" class="wechat-login-button">
    微信登录
  </button>
</template>

<script>
export default {
  methods: {
    handleLogin() {
      // 从环境变量中获取参数
      const appId = import.meta.env.VITE_APP_ID; // 从环境变量中读取 appId
      const redirectUri = encodeURIComponent(import.meta.env.VITE_REDIRECT_URI); // 从环境变量中读取 redirectUri

      const responseType = 'code';
      const scope = 'snsapi_login'; // 网页应用固定填写 snsapi_login

      // 生成一个随机的 state 参数,用于防止 CSRF 攻击
      const state = Math.random().toString(36).substring(2); // 或者使用更安全的方式生成一个随机字符串

      // 拼接请求URL,并加入 state 参数
      const wechatLoginUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=${responseType}&scope=${scope}&state=${state}#wechat_redirect`;

      // 跳转到微信登录页面
      window.location.href = wechatLoginUrl;
    },
  },
};
</script>

<style scoped>
.wechat-login-button {
  background-color: #1aad19;
  color: white;
  border: none;
  border-radius: 5px;
  padding: 10px 20px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.wechat-login-button:hover {
  background-color: #128c13;
}
</style>
3.此时开放平台会弹出一个扫码的页面,用户扫码确认
4.用户确认成功后,开放平台会将code和state作为参数去请求redirectUri(前端页面)
5.前端页面获取code和state,再向后端发送请求
vue 复制代码
<template>
  <div class="login-container">
    <div class="loading-spinner"></div>
    <p class="loading-text">微信登录中,请稍候...</p>
  </div>
</template>

<script>
export default {
  async mounted() {
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get("code");
    const state = urlParams.get("state");

    if (!code) {
      console.error("未获取到微信返回的 code");
      alert("登录失败,请重试");
      return;
    }

    try {
      const response = await fetch("/wx/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ code, state }),
      });

      const result = await response.json();

      if (result.success) {
        const unionid = result.data;
        alert(`登录成功,您的unionid是:${unionid}`);
        this.$router.push({ path: "/products" });
      } else {
        alert("登录失败,请重试");
      }
    } catch (error) {
      console.error("请求失败", error);
      alert("网络错误,请稍后重试");
    }
  },
};
</script>

<style scoped>
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap");

:root {
  --primary-color: #4facfe;
  --secondary-color: #00f2fe;
  --text-color: #333;
}

.login-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background: linear-gradient(120deg, #ffffff, #f0f0f0);
  font-family: "Poppins", sans-serif;
}

.loading-spinner {
  width: 60px;
  height: 60px;
  border: 6px solid #e0e0e0;
  border-top: 6px solid var(--primary-color);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-text {
  margin-top: 20px;
  font-size: 18px;
  font-weight: 500;
  color: var(--text-color);
  animation: fadeIn 2s ease-in-out infinite alternate;
}

@keyframes fadeIn {
  0% {
    opacity: 0.6;
  }
  100% {
    opacity: 1;
  }
}
</style>
6.后端使用code进行微信登录,可以获取到AccessTokenResponse
java 复制代码
/**
 * 微信登录的响应类
 */
@Data
public static class AccessTokenResponse {

    @SerializedName("access_token")
    private String accessToken;

    @SerializedName("expires_in")
    private Integer expiresIn;

    @SerializedName("refresh_token")
    private String refreshToken;

    @SerializedName("openid")
    private String openId;

    @SerializedName("scope")
    private String scope;

    @SerializedName("unionid")
    private String unionId;
}
相关推荐
特视界说1 天前
多规格产品动画怎么规划?把“一个型号一条片”改成可管理的内容架构
人工智能·安全·微信·新浪微博
悟空码字2 天前
视频号直播变了
微信·视频号
随性而行3603 天前
企业微信 Webhook 接收不到消息?网络连通性、URL 签名验签与 API 返回错误码排查指南
java·网络·人工智能·后端·微信·企业微信
未秃头的程序猿4 天前
同一个商户号,V2 免密代扣和 V3 分账的证书怎么共存?
java·后端·微信
木易 士心4 天前
Spring Boot 微信公众号后端服务开发实战指南
spring boot·后端·微信
linda`li6 天前
如何使用TRAE+Figma自动生成前端代码
微信·figma·自动写代码
随性而行3607 天前
企业微信 API 如何实现业务消息自动通知?接口应用场景解析
java·人工智能·后端·微信·机器人·企业微信
极客互动API7 天前
企业微信二次开发 API:如何与 SCRM 打通外部联系人的消息发送
微信·企业微信·企微scrm·企业微信api·企微外部联系人
ZealSinger10 天前
Boot4升后WebClient starter怎么换
starter·webclient·restclient·boot4