微信扫码登录(服务号)实现

微信扫码登录有两种实现方式

  • 方式一:微信开放平台实现,为了接入更多第三方而开放的接口
  • 方式二:微信公众号扫码实现,借助服务号,生成带参数的二维码,实现扫码关注后登录

还有一种场景,H5页面通过点击某个按钮从而获取用户信息,实现登录

前置准备

(开发者ID、开发者密码、服务器地址、令牌、消息加解密密钥)

  • 注册服务号:前往微信公众平台注册
  • 服务器配置:服务号=>设置与开发=>开发接口管理=>服务器配置,修改配置

    • 服务器地址(URL):用于接收服务号推送的消息
    • 令牌(Token):自定义
    • 消息加解密密钥(EncodingAESKey):消息加解密密钥将用于消息体加解密过程
    • 消息加解密方式:明文模式、兼容模式、安全模式

流程

  1. 前端请求后端生成二维码
  2. 后端带上appid、secret和grant_type向微信服务器获取 access_token,并存储到Redis中,设置过期时间
  3. 后端带上expire_seconds、action_name、scene_id向微信服务器获取ticket,并存入Redis中,值设为wait,并设置过期时间
  4. 后端携带 ticket 获取带参二维码,返回二维码url和 ticket 给前端
  5. 前端携带 ticket 使用 轮询获取登录状态
  6. 用户扫码,后端收到回调,获取 openid 和 ticket,将Redis中的ticket值改为openid
  7. 后端执行用户登录逻辑,查询用户信息返回给前端,并保存登录态
  8. 前端收到登录结果更新,正常页面

wxsmdl.png

相关文档

目录结构

image-20250322093200196.png

代码:

RedisConfig

package com.example.demo.config;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
​
​
@Configuration
public class RedisConfig {
​
    @Bean(name = "redisTemplate")
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
​
}
​

WechatMpProperties

package com.example.demo.config;
​
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
​
@Configuration
@ConfigurationProperties(prefix = "wx.mp")
@Data
public class WechatMpProperties {
    private String appId;
    private String secret;
    private String token;
​
    // Getters and Setters
}

Constant

package com.example.demo.constant;
​
public interface Constant {
​
    /**
     * access_token 的缓存key
     */
    String ACCESSTOKEN = "access_token";
​
    /**
     * 用户登录态键
     */
    String USER_LOGIN_STATE = "login:userLogin";
​
}
​

DemoController

package com.example.demo.controller;
​
import com.example.demo.domain.dto.RequestBodyDTO;
import com.example.demo.domain.vo.LoginVO;
import com.example.demo.service.WechatService;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
​
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
​
import java.util.concurrent.TimeUnit;
​
import static com.example.demo.constant.Constant.USER_LOGIN_STATE;
​
@RestController
@Slf4j
public class DemoController {
​
    @Resource
    private StringRedisTemplate redisTemplate;
​
    @Resource
    private WxMpService wxMpService;
​
    @Resource
    private WechatService wechatService;
​
    @Value("${wx.mp.app-id}")
    private String appid;
​
​
    /**
     *验签
     * @param signature
     * @param timestamp
     * @param nonce
     * @param echostr
     * @return
     */
    @GetMapping("/")
    public String authGet(@RequestParam(name = "signature", required = false) String signature,
                          @RequestParam(name = "timestamp", required = false) String timestamp,
                          @RequestParam(name = "nonce", required = false) String nonce,
                          @RequestParam(name = "echostr", required = false) String echostr) {
        log.info("接收微信验证请求:appid={}, signature={}, timestamp={}, nonce={}, echostr={}",
                appid, signature, timestamp, nonce, echostr);
        if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
            log.error("请求参数不完整");
            return "非法请求";
        }
        if (!this.wxMpService.switchover(appid)) {
            log.error("未找到对应appid=[{}]的配置", appid);
            return "非法请求";
        }
        if (wxMpService.checkSignature(timestamp, nonce, signature)) {
            return echostr;
        }
        log.error("签名验证失败");
        return "非法请求";
    }
​
​
    /**
     * 前端获取登录请求的操作
     */
    @GetMapping("/login")
    public LoginVO login() {
        return wechatService.login();
    }
​
​
​
    /**
     * 回调
     */
    @PostMapping(value = "/", produces = "application/xml; charset=UTF-8")
    public String authPost(@RequestBody String requestBody,
                           @RequestParam(name = "signature", required = false) String signature,
                           @RequestParam(name = "timestamp", required = false) String timestamp,
                           @RequestParam(name = "nonce", required = false) String nonce,
                           @RequestParam(name = "openid", required = false) String openid,
                           @RequestParam(name = "encrypt_type", required = false) String encType,
                           @RequestParam(name = "msg_signature", required= false) String msgSignature) {
        try {
            log.info("接收微信验证请求:openid={}, signature={}, timestamp={}, nonce={}", openid, signature, timestamp, nonce);
//            消息转换
            XmlMapper xmlMapper = new XmlMapper();
            RequestBodyDTO message = xmlMapper.readValue(requestBody, RequestBodyDTO.class);
//            扫码登录【消息类型和事件】
            if ("event".equals(message.getMsgType()) && "SCAN".equals(message.getEvent())) {
                redisTemplate.opsForValue().set(message.getTicket(), openid, 1, TimeUnit.MINUTES);
//            登录成功
//                推送登录成功消息
            }
            log.info("接收微信公众号信息请求{}完成{}", openid, requestBody);
        }catch (Exception e) {
            log.error("微信回调异常", e);
        }
        return "";
    }
​
​
    /**
     * 结果查询
     */
    @GetMapping("/checkLogin")
    public String checkLogin(@RequestParam String ticket, HttpServletRequest request) {
        if (ticket == null) {
            return "未登录";
        }
        String openId = redisTemplate.opsForValue().get(ticket);
        if (openId == null) {
            return "二维码已失效";
        }
 //     判断值是否为wait,如果为wait,则返回未登录,否则返回openId
        if ("wait".equals(openId)) {
            return "未登录";
        }
//        todo 查询openId对应的用户信息
//        登录操作,存储登录态
        request.getSession().setAttribute(USER_LOGIN_STATE, openId);
        return openId;
    }
​
    /**
     * 获取当前登录信息
     */
    @GetMapping("/getCurrentUser")
    public String getCurrentUser(HttpServletRequest request) {
        Object user = request.getSession().getAttribute(USER_LOGIN_STATE);
        if (user == null) {
            return "未登录";
        }
        return user.toString();
    }
}
​

GetAccessTokenRequestDTO

package com.example.demo.domain.dto;
​
import lombok.Data;
​
@Data
public class GetAccessTokenRequestDTO {
​
    /**
     * 授权模式
     */
    private String grant_type;
​
​
    /**
     * 应用ID
     */
    private String appid;
​
​
    /**
     * 应用密钥
     */
    private String secret;
​
​
}
​

RequestBodyDTO

package com.example.demo.domain.dto;
​
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
​
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class RequestBodyDTO {
​
    @JsonProperty("ToUserName")
    private String toUserName;
    @JsonProperty("FromUserName")
    private String fromUserName;
    @JsonProperty("CreateTime")
    private String createTime;
    @JsonProperty("MsgType")
    private String msgType;
    @JsonProperty("Event")
    private String event;
    @JsonProperty("EventKey")
    private String eventKey;
    @JsonProperty("Ticket")
    private String ticket;
​
}
​

WechatQrCodeResponseDTO

package com.example.demo.domain.dto;
​
import lombok.Data;
​
@Data
public class WechatQrCodeResponseDTO {
​
    /**
     * 临时二维码的ticket,凭借此ticket可以在有效时间内换取二维码。
     */
    private String ticket;
​
    /**
     * 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。
     */
    private String expire_seconds;
​
    /**
     * 二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片
     */
    private String url;
​
}
​

WechatTokenResponseDTO

package com.example.demo.domain.dto;
​
import lombok.Data;
​
@Data
public class WechatTokenResponseDTO {
​
    /**
     * 获取到的凭证
     */
    private String access_token;
​
    /**
     * 凭证有效时间,单位:秒
     */
    private Long expires_in;
​
}
​

LoginVO

package com.example.demo.domain.vo;
​
import lombok.Data;
​
@Data
public class LoginVO {
​
    /**
     * 扫码返回的ticket
     */
    private String ticket;
​
    /**
     * 二维码图片的url
     */
    private String url;
​
}
​

WechatService

package com.example.demo.service;
​
import com.example.demo.domain.dto.GetAccessTokenRequestDTO;
import com.example.demo.domain.dto.WechatQrCodeResponseDTO;
import com.example.demo.domain.dto.WechatTokenResponseDTO;
import com.example.demo.domain.vo.LoginVO;
​
public interface WechatService {
​
    /**
     * 获取AccessToken
     */
    WechatTokenResponseDTO getAccessToken(String appId);
​
​
​
    /**
     * 获取ticket凭证
     */
    WechatQrCodeResponseDTO createQRCode(String accessToken, int sceneId, int expireSeconds);
​
​
    /**
     * 获取带参数的二维码图片
     */
    String getQRCodeImageUrl(String ticket);
​
​
    /**
     * 获取登录凭证
     * @return
     */
    LoginVO login();
}
​

WechatServiceImpl

package com.example.demo.service.impl;
​
import com.example.demo.domain.dto.WechatQrCodeResponseDTO;
import com.example.demo.domain.dto.WechatTokenResponseDTO;
import com.example.demo.domain.vo.LoginVO;
import com.example.demo.service.WechatService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
​
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
​
import static com.example.demo.constant.Constant.ACCESSTOKEN;
​
@Service
public class WechatServiceImpl implements WechatService {
​
    private static final Logger log = LoggerFactory.getLogger(WechatServiceImpl.class);
​
    @Resource
    private WxMpService wxMpService;
​
    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
    private final RestTemplate restTemplate = new RestTemplate();
    private final ObjectMapper objectMapper = new ObjectMapper();
​
​
    @Value("${wx.mp.app-id}")
    private String appId;
​
    /**
     * 获取登录凭证
     * @return
     */
    @Override
    public LoginVO login() {
        String accessToken = null;
//        查询redis中是否有accessToken,如果有则返回,如果没有则调用接口获取
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(ACCESSTOKEN))) {
            accessToken = stringRedisTemplate.opsForValue().get(ACCESSTOKEN);
        }else {
            String access_token = getAccessToken(appId).getAccess_token();
            stringRedisTemplate.opsForValue().set(ACCESSTOKEN, access_token, 1, TimeUnit.HOURS);
            accessToken = access_token;
        }
//        请求ticket
        WechatQrCodeResponseDTO qrCode = createQRCode(accessToken, 1, 1800);
        String ticket = qrCode.getTicket();
//        将ticket存储到redis
        stringRedisTemplate.opsForValue().set(ticket, "wait", 2, TimeUnit.MINUTES);
//        请求获取带参码的二维码图片的url
        String qrCodeImageUrl = getQRCodeImageUrl(ticket);
//        返回ticket和url
        LoginVO loginVO = new LoginVO();
        loginVO.setTicket(ticket);
        loginVO.setUrl(qrCodeImageUrl);
        return loginVO;
    }
​
    /**
     * 获取微信 access_token
     * @return
     */
    @Override
    public WechatTokenResponseDTO getAccessToken(String appId) {
     WechatTokenResponseDTO wechatTokenResponseDTO = new WechatTokenResponseDTO();
     if (!this.wxMpService.switchover(appId)) {
         log.error("未找到对应appid=[{}]的配置", appId);
         return null;
     }
     try {
         String accessToken = wxMpService.getAccessToken();
         wechatTokenResponseDTO.setAccess_token(accessToken);
         wechatTokenResponseDTO.setExpires_in(wxMpService.getWxMpConfigStorage().getExpiresTime());
         return wechatTokenResponseDTO;
     } catch (WxErrorException e) {
         throw new RuntimeException(e);
     }
    }
​
    /**
     * 创建临时二维码
     * @param accessToken
     * @param sceneId
     * @param expireSeconds
     * @return
     */
    @Override
    public WechatQrCodeResponseDTO createQRCode(String accessToken, int sceneId, int expireSeconds) {
        WechatQrCodeResponseDTO wechatQrCodeResponseDTO = new WechatQrCodeResponseDTO();
//        创建临时二维码
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;
        String requestBody = String.format("{\"expire_seconds\":%d,\"action_name\":\"QR_SCENE\",\"action_info\":{\"scene\":{\"scene_id\":%d}}}", expireSeconds, sceneId);
        String response = restTemplate.postForObject(url, requestBody, String.class);
        log.info("requestBody: {}", response);
​
        try {
            JsonNode jsonNode = objectMapper.readTree(response);
            System.out.println("response: " + response);
            String ticket = jsonNode.get("ticket").asText(); // 获取ticket
            String expireSecondsStr = jsonNode.get("expire_seconds").asText(); // 获取expire_seconds
            String urlStr = jsonNode.get("url").asText();
            wechatQrCodeResponseDTO.setTicket(ticket);
            wechatQrCodeResponseDTO.setExpire_seconds(expireSecondsStr);
            wechatQrCodeResponseDTO.setUrl(urlStr);
            return wechatQrCodeResponseDTO;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
​
    /**
     * 获取二维码图片的url
     * @param ticket
     * @return
     */
    @Override
    public String getQRCodeImageUrl(String ticket) {
        if (ticket != null) {
            return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
        }
        return null;
    }
​
}
​

Application.yml

server:
  port: 8080
wx:
  mp:
    app-id: "XXX"
    secret: "XXX"
    token: XXX
​
spring:
  session:
    timeout: 60480000
    store-type: redis
  redis:
    database: 1
    host: XX.XX.XX.XX
    port: 6379
    password: "XXX"
最后修改:2025 年 03 月 24 日
如果觉得我的文章对你有用,请随意赞赏