文章目录
-
- 1.资质申请
- 2.combinations-wx-login-starter
-
-
- 1.目录结构
- [2.pom.xml 引入okhttp依赖](#2.pom.xml 引入okhttp依赖)
- [3.WxLoginProperties.java 属性配置](#3.WxLoginProperties.java 属性配置)
- [4.WxLoginUtil.java 后端通过 code 获取 access_token的工具类](#4.WxLoginUtil.java 后端通过 code 获取 access_token的工具类)
- [5.WxLoginAutoConfiguration.java 自动配置类](#5.WxLoginAutoConfiguration.java 自动配置类)
- [6.spring.factories 激活自动配置类](#6.spring.factories 激活自动配置类)
- 3.combinations-wx-starter-demo
-
-
- 1.目录结构
- [2.pom.xml 引入依赖](#2.pom.xml 引入依赖)
- [3.application.yml 配置AppID和AppSecret](#3.application.yml 配置AppID和AppSecret)
- [4.application-prod.yml 配置生产环境的日志和.env文件路径](#4.application-prod.yml 配置生产环境的日志和.env文件路径)
- [5.CodeAndState.java 接受code和state的bean](#5.CodeAndState.java 接受code和state的bean)
- [6.WxLoginController.java 微信登录Controller](#6.WxLoginController.java 微信登录Controller)
- [7.WxApplication.java 启动类](#7.WxApplication.java 启动类)
- 4.微信登录流程梳理
-
1.资质申请
- 主体为企业的域名和备案的服务器
- 主体为企业的微信开放平台的开发者资质认证
- 微信开放平台创建应用获取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;
}