微信小程序授权登录实现
技术栈
- 后端: Spring Boot + MyBatis Plus + JWT
- 前端: Vue 3 + uni-app
- 认证: 微信 openid + JWT Token (7天有效期)
核心流程
点击头像 → 选择头像 → 获取code → 后端换openid → 检查用户
├─ 老用户 → 直接登录
└─ 新用户 → 输入昵称 → 注册登录
后端实现
1. JWT工具类 (JwtUtil.java)
// 生成Token (包含 userId 和 openid, 有效期7天)
public static String generateToken(Long userId, String openid)
// 验证Token
public static boolean validateToken(String token)
// 提取用户信息
public static Long getUserIdFromToken(String token)
public static String getOpenidFromToken(String token)
⚠️ 生产环境必须修改 SECRET_KEY
2. 登录接口 (AuthController.java)
@PostMapping("/auth/wxLogin")
public Result wxLogin(@RequestBody WxLoginRequest request) {
// 1. 调用微信API换取openid
String openid = getOpenidByCode(request.getCode());
// 2. 检查用户是否存在
User user = userService.getOne(wrapper.eq("openid", openid));
boolean isNewUser = (user == null);
// 3. 新用户首次请求,返回isNewUser标志
if (isNewUser && (nickname == null || avatarUrl == null)) {
result.put("isNewUser", true);
return Result.success(result);
}
// 4. 创建新用户或更新现有用户
if (user == null) {
user = new User();
user.setOpenid(openid);
user.setNickname(nickname);
user.setAvatarUrl(avatarUrl);
userService.save(user);
} else if (nickname != null && avatarUrl != null) {
user.setNickname(nickname);
user.setAvatarUrl(avatarUrl);
userService.updateById(user);
}
// 5. 生成Token
String token = JwtUtil.generateToken(user.getUserId(), openid);
result.put("isNewUser", false);
result.put("token", token);
result.put("userInfo", user);
return Result.success(result);
}
// 获取openid
private String getOpenidByCode(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + appId // ⚠️ 配置真实AppID
+ "&secret=" + appSecret // ⚠️ 配置真实AppSecret
+ "&js_code=" + code
+ "&grant_type=authorization_code";
String response = restTemplate.getForObject(url, String.class);
JSONObject json = JSON.parseObject(response);
return json.getString("openid");
}
3. User实体类
@TableId(value = "user_id", type = IdType.AUTO) // ⚠️ 必须AUTO
private Long userId;
private String openid;
private String nickname;
private String avatarUrl;
前端实现
1. 工具函数 (utils/auth.js)
export const setToken = (token) => uni.setStorageSync('uni_shop_token', token)
export const getToken = () => uni.getStorageSync('uni_shop_token')
export const isLoggedIn = () => !!getToken()
export const setUserInfo = (info) => uni.setStorageSync('uni_shop_userInfo', info)
export const clearAuth = () => {
uni.removeStorageSync('uni_shop_token')
uni.removeStorageSync('uni_shop_userInfo')
}
2. 请求拦截器 (utils/request.js)
const request = (options) => {
const token = getToken()
const header = {
'Content-Type': 'application/json',
...(token && { 'Authorization': token })
}
return uni.request({
url: BASE_URL + options.url,
method: options.method,
data: options.data,
header,
success: (res) => {
if (res.statusCode === 401) {
clearAuth()
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
}
3. 登录逻辑 (MyIndex/myIdex.vue)
<button
v-if="!isLoggedIn()"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<image src="/static/logo.png"></image>
<text>点击登录</text>
</button>
const onChooseAvatar = async (e) => {
tempAvatar.value = e.detail.avatarUrl
// 第一步:检查用户状态
uni.showLoading({ title: '检查登录状态...' })
const loginRes = await uni.login()
const checkRes = await wxLogin({ code: loginRes.code })
uni.hideLoading()
// 第二步:根据isNewUser决定流程
if (!checkRes.data.isNewUser) {
// 老用户 - 直接登录
setToken(checkRes.data.token)
setUserInfo(checkRes.data.userInfo)
userInfo.value = checkRes.data.userInfo
uni.showToast({ title: '登录成功', icon: 'success' })
} else {
// 新用户 - 弹出昵称输入框
uni.showModal({
title: '请输入昵称',
editable: true,
success: async (res) => {
if (res.confirm && res.content) {
// 第三步:完成注册
const finalRes = await wxLogin({
code: loginRes.code,
nickname: res.content.trim(),
avatarUrl: tempAvatar.value
})
setToken(finalRes.data.token)
setUserInfo(finalRes.data.userInfo)
userInfo.value = finalRes.data.userInfo
uni.showToast({ title: '登录成功', icon: 'success' })
}
}
})
}
}
配置
1. 获取微信凭证
- 登录 微信公众平台
- 开发 → 开发管理 → 开发设置
- 复制 AppID 和 AppSecret
2. 后端配置
// AuthController.java
String appId = "※※※※※";
String appSecret = "73854ccd517a9d36...";
// JwtUtil.java
private static final String SECRET_KEY = "复杂密钥至少32位";
3. 前端配置
// utils/request.js
const BASE_URL = 'http://localhost:9090/'
测试
测试步骤
- 微信开发者工具打开项目
- 点击"我的" → 点击头像
- 选择头像
- 新用户: 输入昵称 → 登录
- 老用户: 直接登录(无昵称弹窗)
常见问题
| 问题 | 解决方案 |
|---|---|
| 登录失败 | 检查后端是否启动(netstat -ano | findstr :9090) |
| 获取openid失败 | 检查AppID和AppSecret是否正确 |
| Token失效 | 清除旧Token:uni.removeStorageSync('uni_shop_token') |
| 用户未保存 | 检查 User.java的 @TableId(type = IdType.AUTO) |
获取用户ID进行业务操作
方式一: 前端从本地存储获取
// 获取完整用户信息(包含userId)
import { getUserInfo } from '@/utils/auth'
const userInfo = getUserInfo()
const userId = userInfo.userId
// 用于后续业务请求
const orderRes = await request({
url: '/order/list',
method: 'GET',
data: { userId } // 不推荐,应该从后端Token解析
})
⚠️ 不推荐: 前端传userId不安全,容易被篡改
方式二: 后端从Token解析 (推荐)
// 创建拦截器或使用 @RequestHeader
@GetMapping("/order/list")
public Result getOrderList(@RequestHeader("Authorization") String token) {
// 从Token中提取userId
Long userId = JwtUtil.getUserIdFromToken(token);
// 使用userId查询业务数据
List<Order> orders = orderService.list(
new QueryWrapper<Order>().eq("user_id", userId)
);
return Result.success(orders);
}
方式三: 使用登录拦截器 (最佳实践)
创建拦截器 (LoginInterceptor.java):
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (token == null || !JwtUtil.validateToken(token)) {
response.setStatus(401);
return false;
}
// 解析并存储userId到请求属性
Long userId = JwtUtil.getUserIdFromToken(token);
request.setAttribute("userId", userId);
return true;
}
}
注册拦截器 (WebConfig.java):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/**"); // 排除登录接口
}
}
在Controller中使用:
@GetMapping("/order/list")
public Result getOrderList(HttpServletRequest request) {
// 直接从request中获取userId
Long userId = (Long) request.getAttribute("userId");
List<Order> orders = orderService.list(
new QueryWrapper<Order>().eq("user_id", userId)
);
return Result.success(orders);
}
@PostMapping("/cart/add")
public Result addToCart(@RequestBody CartRequest req,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
Cart cart = new Cart();
cart.setUserId(userId);
cart.setProductId(req.getProductId());
cart.setQuantity(req.getQuantity());
cartService.save(cart);
return Result.success("添加成功");
}
方式四: 使用ThreadLocal (高级) ⚠️ 仍需要拦截器
重要: ThreadLocal 只是存储容器,必须配合拦截器使用!
创建用户上下文 (UserContext.java):
public class UserContext {
private static ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
public static void setUserId(Long userId) {
userIdHolder.set(userId);
}
public static Long getUserId() {
return userIdHolder.get();
}
public static void clear() {
userIdHolder.remove();
}
}
必须配合拦截器使用 (LoginInterceptor.java):
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
// 1. 验证Token
if (token == null || !JwtUtil.validateToken(token)) {
response.setStatus(401);
return false;
}
// 2. 解析userId并存入ThreadLocal
Long userId = JwtUtil.getUserIdFromToken(token);
UserContext.setUserId(userId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 3. ⚠️ 必须清理,防止内存泄漏
UserContext.clear();
}
}
注册拦截器 (WebConfig.java):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/**");
}
}
在任何地方使用:
@Service
public class OrderServiceImpl implements OrderService {
public List<Order> getMyOrders() {
// 直接获取当前登录用户ID
Long userId = UserContext.getUserId();
return orderMapper.selectList(
new QueryWrapper<Order>().eq("user_id", userId)
);
}
}
ThreadLocal vs 拦截器 request.setAttribute 的区别:
- request.setAttribute: 只能在Controller层获取,Service层需要手动传递
- ThreadLocal: 可在Service/Mapper等任意层直接获取,无需传参
主要特性
✅ 老用户免输昵称,直接登录
✅ 新用户弹窗输入昵称
✅ JWT Token自动管理
✅ Token过期自动跳转
✅ 登录状态持久化(7天)
✅ 后端统一从Token解析用户ID
更新: 2025年11月17日