200字
微信小程序授权登录实现
2025-11-20
2025-11-21

微信小程序授权登录实现

技术栈

  • 后端: 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. 获取微信凭证

  1. 登录 微信公众平台
  2. 开发 → 开发管理 → 开发设置
  3. 复制 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/'

测试

测试步骤

  1. 微信开发者工具打开项目
  2. 点击"我的" → 点击头像
  3. 选择头像
  4. 新用户: 输入昵称 → 登录
  5. 老用户: 直接登录(无昵称弹窗)

常见问题

问题 解决方案
登录失败 检查后端是否启动(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日


评论