springboot+vue 实现微信扫码登录系统

5 阅读4分钟

微信扫码登录的核心流程是:

  1. 前端展示微信登录二维码(来自后端生成的 URL)。
  2. 用户用微信扫描二维码,在微信客户端授权。
  3. 微信服务器回调后端接口,携带 code
  4. 后端用 code换取 access_token和用户信息。
  5. 后端将用户信息与系统账号绑定(或自动注册),生成自己的登录态(如 JWT Token)。
  6. 前端轮询或 WebSocket 等方式得知登录成功,拿到 Token,完成登录。

一、准备工作(微信开放平台)

  1. 注册微信开放平台账号

    open.weixin.qq.com/

  2. 创建网站应用

    • 填写网站域名(如 www.yoursite.com
    • 获取 AppID​ 和 AppSecret
  3. 配置授权回调域名

    在开放平台后台设置 授权回调域名(不带 http://https://),例如 www.yoursite.com


二、整体流程设计

┌──────────────┐          ┌──────────────┐
│   前端 Vue   │          │ Spring Boot │
└─────┬────────┘          └─────┬────────┘
      │                        │
      │ 1.请求微信登录URL       │
      │───────────────────────>│
      │                        │ 2.生成state,构造微信授权URL
      │<───────────────────────│
      │ 3.展示二维码(微信URL)   │
      │                        │
      │ 4.用户扫码授权          │
      │                        │
      │                        │ 5.微信回调 /wechat/callback?code=xxx&state=yyy
      │                        │ 6.用code换access_token和用户信息
      │                        │ 7.查找或创建系统用户,生成JWT Token
      │                        │ 8.返回登录成功页面(带Token)
      │ 9.前端拿到Token,登录成功│

三、后端 Spring Boot 实现

1. 添加依赖(pom.xml

<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- HTTP Client -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

<!-- JSON解析 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.40</version>
</dependency>

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2. 配置文件 application.yml

wechat:
  appid: your_appid
  secret: your_secret
  redirect-uri: https://www.yoursite.com/wechat/callback
  scope: snsapi_login
  state: random_state_string

3. 微信配置类

@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WeChatConfig {
    private String appid;
    private String secret;
    private String redirectUri;
    private String scope;
    private String state;
}

4. 微信服务类(核心逻辑)

@Service
public class WeChatService {

    @Autowired
    private WeChatConfig weChatConfig;

    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
    private static final String USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";

    /**
     * 生成微信授权URL
     */
    public String getWeChatAuthUrl() {
        return "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=" + weChatConfig.getAppid() +
                "&redirect_uri=" + URLEncoder.encode(weChatConfig.getRedirectUri(), StandardCharsets.UTF_8) +
                "&response_type=code" +
                "&scope=" + weChatConfig.getScope() +
                "&state=" + weChatConfig.getState();
    }

    /**
     * 用 code 换 access_token
     */
    public JSONObject getAccessToken(String code) throws Exception {
        String url = ACCESS_TOKEN_URL +
                "?appid=" + weChatConfig.getAppid() +
                "&secret=" + weChatConfig.getSecret() +
                "&code=" + code +
                "&grant_type=authorization_code";
        String result = httpGet(url);
        return JSON.parseObject(result);
    }

    /**
     * 用 access_token 和 openid 获取用户信息
     */
    public JSONObject getUserInfo(String accessToken, String openid) throws Exception {
        String url = USER_INFO_URL +
                "?access_token=" + accessToken +
                "&openid=" + openid;
        String result = httpGet(url);
        return JSON.parseObject(result);
    }

    private String httpGet(String url) throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);
        try (CloseableHttpResponse response = client.execute(httpGet)) {
            return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
        }
    }
}

5. 控制器

@RestController
@RequestMapping("/wechat")
public class WeChatController {

    @Autowired
    private WeChatService weChatService;

    /**
     * 第一步:前端请求微信登录URL
     */
    @GetMapping("/login-url")
    public Map<String, String> getLoginUrl() {
        String authUrl = weChatService.getWeChatAuthUrl();
        return Collections.singletonMap("url", authUrl);
    }

    /**
     * 第二步:微信回调
     */
    @GetMapping("/callback")
    public void callback(@RequestParam String code,
                         @RequestParam String state,
                         HttpServletResponse response) throws Exception {
        // 1. 用 code 换 token
        JSONObject tokenJson = weChatService.getAccessToken(code);
        String accessToken = tokenJson.getString("access_token");
        String openid = tokenJson.getString("openid");

        // 2. 获取用户信息
        JSONObject userInfo = weChatService.getUserInfo(accessToken, openid);
        String nickname = userInfo.getString("nickname");
        String headimgurl = userInfo.getString("headimgurl");

        // 3. 根据 openid 查找或创建系统用户(这里省略具体业务逻辑)
        User user = userService.findOrCreateByWeChat(openid, nickname, headimgurl);

        // 4. 生成系统 Token(JWT)
        String token = JwtUtil.generateToken(user.getId());

        // 5. 重定向到前端登录成功页面,带上 token
        response.sendRedirect("https://www.yoursite.com/login-success?token=" + token);
    }
}

6. JWT 工具类(示例)

public class JwtUtil {
    private static final String SECRET = "your_jwt_secret";
    private static final long EXPIRATION = 86400000; // 1天

    public static String generateToken(Long userId) {
        return Jwts.builder()
                .setSubject(userId.toString())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    public static Long parseUserId(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
        return Long.valueOf(claims.getSubject());
    }
}

四、前端 Vue 实现

1. 安装 axios

npm install axios

2. 登录页面组件

<template>
  <div>
    <h2>微信扫码登录</h2>
    <img :src="qrCodeUrl" alt="微信登录二维码" v-if="qrCodeUrl" />
    <p v-else>加载中...</p>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      qrCodeUrl: ''
    };
  },
  mounted() {
    this.getQrCode();
  },
  methods: {
    async getQrCode() {
      const res = await axios.get('/api/wechat/login-url');
      // 这里可以用 qrcode.vue 等库把 URL 转成二维码图片
      this.qrCodeUrl = res.data.url;
    }
  }
};
</script>

3. 登录成功页面

<template>
  <div>
    <h2>登录成功</h2>
    <p>欢迎 {{ user.nickname }}</p>
    <img :src="user.headimgurl" alt="头像" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {}
    };
  },
  created() {
    // 从 URL 参数获取 token
    const token = this.$route.query.token;
    if (token) {
      localStorage.setItem('token', token);
      // 调用接口获取用户信息
      this.fetchUser();
    }
  },
  methods: {
    async fetchUser() {
      const res = await axios.get('/api/user/info', {
        headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
      });
      this.user = res.data;
    }
  }
};
</script>

五、安全与优化建议

  1. State 防 CSRF

    生成随机 state 并存入 session/redis,回调时校验,防止跨站请求伪造。

  2. Token 安全

    • 使用 HTTPS
    • 设置合理的过期时间
    • 刷新 Token 机制
  3. 用户绑定

    • 微信 openid 与系统账号绑定
    • 支持手机号/密码等其他方式注册绑定
  4. 前端轮询备选

    如果不用重定向,可以让前端轮询 /api/wechat/check-login?state=xxx判断登录状态。

  5. 日志与异常处理

    记录微信接口调用日志,处理 access_token 失效、用户拒绝授权等情况。


六、总结

  • Spring Boot 负责:生成微信授权 URL、处理回调、换取 token 和用户信息、生成本系统登录态。
  • Vue 负责:展示二维码、跳转微信授权、接收登录成功后的 Token、完成前端登录态保存。
  • 关键点:微信开放平台配置、回调域名、state 校验、Token 管理、用户绑定逻辑。