
当用户面对一个新应用时,85%的人会因“注册流程繁琐”放弃使用——而集成微信登录后,这一数据可降低至15%(来源:腾讯云开发者报告)。无需记忆密码、1步完成认证,微信登录已成为提升用户转化率的“标配功能”。本文将以Spring Boot 2.7.x为基础,从0到1实现微信登录,涵盖账号申请、代码开发、前后端交互、安全防护全流程,帮你避开90%的集成陷阱。
微信登录基于OAuth2.0授权码模式,本质是“第三方应用通过微信间接获取用户信息”的安全流程。你可以将其类比为:
用户(你) 入住酒店(第三方应用),无需告知酒店钥匙(密码),而是让酒店向前台(微信开放平台)申请临时房卡(code),再用房卡换正式房卡(access_token),最终打开房门(获取用户信息)。

关键角色:
工具/依赖版本要求用途JDK1.8+运行环境Spring Boot2.5.x-2.7.x项目框架Maven3.6+依赖管理Redis6.0+存储临时code和token内网穿透工具ngrok/cpolar本地开发时,让微信回调能访问到本地服务
在pom.xml中引入HTTP客户端、JSON解析等工具:
xml
<!-- 微信登录核心依赖 -->
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- HTTP客户端 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- JSON解析 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
<!-- Redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
在application.yml中添加:
yaml
wechat:
app-id: wx1234567890abcdef # 替换为开放平台的AppID
app-secret: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 # 替换为AppSecret
redirect-uri: https://yourdomain.com/login/callback # 回调地址(需与开放平台配置一致,且URL编码)
scope: snsapi_userinfo # 授权作用域,获取用户信息需用此值
spring:
redis:
host: localhost
port: 6379
database: 0
创建WechatConfig.java统一管理参数:
java
@Component
@ConfigurationProperties(prefix = "wechat")
@Data // Lombok注解,自动生成getter/setter
public class WechatConfig {
private String appId;
private String appSecret;
private String redirectUri;
private String scope;
}
创建WechatLoginController,提供生成授权URL的接口,用户访问后会跳转至微信扫码页:
java
@RestController
@RequestMapping("/api/wechat")
public class WechatLoginController {
@Autowired
private WechatConfig wechatConfig;
/**
* 生成微信登录二维码URL
*/
@GetMapping("/login-url")
public String getLoginUrl() throws UnsupportedEncodingException {
// 1. 生成随机state(防CSRF攻击,提议用UUID)
String state = UUID.randomUUID().toString().replace("-", "");
// 2. 对redirect_uri进行URL编码(微信要求)
String encodedRedirectUri = URLEncoder.encode(wechatConfig.getRedirectUri(), "UTF-8");
// 3. 拼接微信授权URL
String loginUrl = String.format(
"https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect",
wechatConfig.getAppId(),
encodedRedirectUri,
wechatConfig.getScope(),
state
);
// 4. 将state存入Redis,有效期5分钟(用于后续验证)
redisTemplate.opsForValue().set("wechat_state:" + state, state, 300, TimeUnit.SECONDS);
return loginUrl;
}
}
用户扫码确认后,微信会重定向到redirect-uri,并携带code和state参数,后端需用code换取access_token:
java
/**
* 微信回调接口(用户扫码后触发)
*/
@GetMapping("/login/callback")
public String handleCallback(@RequestParam String code, @RequestParam String state, HttpServletResponse response) throws IOException {
// 1. 验证state(防CSRF)
String storedState = redisTemplate.opsForValue().get("wechat_state:" + state);
if (storedState == null || !storedState.equals(state)) {
throw new RuntimeException("state验证失败,可能是CSRF攻击");
}
// 2. 用code换取access_token和openid
String tokenUrl = String.format(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
wechatConfig.getAppId(),
wechatConfig.getAppSecret(),
code
);
// 发送HTTP请求获取token(用HttpClient)
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(tokenUrl);
CloseableHttpResponse tokenResponse = httpClient.execute(httpGet);
String tokenJson = EntityUtils.toString(tokenResponse.getEntity());
// 解析JSON(用Gson)
Gson gson = new Gson();
WechatTokenDTO tokenDTO = gson.fromJson(tokenJson, WechatTokenDTO.class);
// 检查是否返回错误(如code无效)
if (tokenDTO.getErrcode() != null) {
throw new RuntimeException("获取access_token失败:" + tokenDTO.getErrmsg());
}
// 3. 用access_token获取用户信息
String userInfoUrl = String.format(
"https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN",
tokenDTO.getAccess_token(),
tokenDTO.getOpenid()
);
HttpGet userInfoGet = new HttpGet(userInfoUrl);
CloseableHttpResponse userInfoResponse = httpClient.execute(userInfoGet);
String userInfoJson = EntityUtils.toString(userInfoResponse.getEntity());
WechatUserInfoDTO userInfo = gson.fromJson(userInfoJson, WechatUserInfoDTO.class);
// 4. 生成自定义登录态(JWT)
String jwtToken = JwtUtils.generateToken(userInfo.getOpenid()); // 自定义JWT工具类
// 5. 重定向到前端页面,携带JWT
response.sendRedirect("https://yourdomain.com/login-success?token=" + jwtToken);
return null;
}
// 数据传输对象(DTO)示例
@Data
public class WechatTokenDTO {
private String access_token; // 访问令牌
private int expires_in; // 有效期(秒,一般7200)
private String refresh_token; // 刷新令牌
private String openid; // 用户唯一标识(同一应用内唯一)
private String scope;
private String unionid; // 多端统一标识(需在开放平台绑定多个应用)
private Integer errcode; // 错误码(成功时为null)
private String errmsg; // 错误信息
}
用户扫码后,前端需存储JWT并处理过期逻辑,用Axios拦截器实现无感刷新:
javascript
// 前端请求拦截器:携带token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('wx_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:处理token过期
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 若401且未尝试刷新
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 调用刷新接口(用refresh_token换access_token)
const { data } = await axios.post('/api/refresh-token');
localStorage.setItem('wx_token', data.newToken);
// 重试原请求
originalRequest.headers.Authorization = `Bearer ${data.newToken}`;
return axios(originalRequest);
} catch (e) {
// 刷新令牌过期,跳转登录
localStorage.removeItem('wx_token');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
java
@PostMapping("/api/refresh-token")
public ResponseEntity<?> refreshToken(@RequestParam String refreshToken) {
// 验证refresh_token(从微信开放平台获取时已存储)
String verifyUrl = String.format(
"https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s",
wechatConfig.getAppId(),
refreshToken
);
// 调用微信接口验证refresh_token
// ...(类似获取access_token的逻辑)
// 验证成功则生成新JWT
String newJwt = JwtUtils.generateToken(openid);
return ResponseEntity.ok(Map.of("newToken", newJwt));
}
缘由:微信开放平台配置的授权回调域与实际回调URL不一致(如配置的是yourdomain.com,实际用了www.yourdomain.com)。解决:
缘由:code是临时凭证(有效期10分钟,且只能使用1次),可能因网络延迟导致重复请求。解决:
方案:使用UnionID——同一用户在微信开放平台下的多个应用中,UnionID一样。需在开放平台“管理中心→绑定公众号/小程序”,代码中优先用UnionID查询用户:
java
// 用户信息入库时优先用UnionID
if (userInfo.getUnionid() != null) {
User existingUser = userMapper.selectByUnionId(userInfo.getUnionid());
} else {
User existingUser = userMapper.selectByOpenid(userInfo.getOpenid()); // 兼容单应用
}
从开放平台申请到前后端token交互,微信登录集成看似简单,实则涉及授权流程、安全防护、用户体验等多方面考量。你的项目中是否遇到过“code已使用”“UnionID获取失败”等问题?欢迎在评论区分享你的解决方案,或提出疑问,我会一一解答!
感谢关注【AI码力】,获取更多实战秘籍!