微信开放平台,网站应用扫码登录 (绑定用户),两种方式


一、微信外置链接二维码的方式

1.准备工作

在这里插入图片描述

开发配置:调试的时候使用:授权回调域
在这里插入图片描述

2. 后端

2.1 后端配置yml

wx:
  open:
    appid: wx??????????  # 替换为你的appid
    appsecret: d747f????????????????  # 替换为你的appsecret
    #redirect-uri: http://????????????????/api/wx/login/callback  # 替换为已配置的回调地址    
    #redirect-uri: http://????????????????/base/wx/head/login/callback  # 替换为已配置的回调地址    
    #redirect-uri: https://????????????????/base/wx/head/login/callback  # 替换为已配置的回调地址    
    redirect-uri: http://????????????????/base/wx/head/login/callback  # 替换为已配置的回调地址    
    scope: snsapi_login  # 网站应用固定为snsapi_login

2.2后端依赖pom.xml

<dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-open</artifactId>
            <version>4.7.0</version>
        </dependency>

2.3后端实现代码

1.WxOpenConfig


import lombok.Data;
import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
import me.chanjar.weixin.open.api.impl.WxOpenOAuth2ServiceImpl;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "wx.open")
public class WxOpenConfig {

    private String appid;

    private String appsecret;

    private String redirectUri;

    private String scope;

    @Bean
    public WxOpenOAuth2ServiceImpl wxOpenOAuth2ServiceImpl() {


        WxOpenInMemoryConfigStorage configStorage = new WxOpenInMemoryConfigStorage();

        // 使用RedisTemplate存储配置
        configStorage.setComponentAppId(appid);
        configStorage.setComponentAppSecret(appsecret);
        // 设置Redis前缀(可选)

        WxOpenOAuth2ServiceImpl service = new WxOpenOAuth2ServiceImpl(appid, appsecret, configStorage);
        service.setWxOpenConfigStorage(configStorage);

        return service;
    }
}

2.controller



import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.google.gson.annotations.SerializedName;
import com.ulinkle.base.api.BaseOrgService;
import com.ulinkle.base.api.BaseStaffService;
import com.ulinkle.base.api.LoginService;
import com.ulinkle.base.config.WxOpenConfig;
import com.ulinkle.common.core.constant.Constants;
import com.ulinkle.common.core.exception.CheckedException;
import com.ulinkle.common.core.utils.ArgumentAssert;
import com.ulinkle.common.core.utils.BeanPlusUtil;
import com.ulinkle.common.core.utils.StrPool;
import com.ulinkle.common.core.web.page.DataResponse;
import com.ulinkle.domain.base.entity.BaseStaff;
import com.ulinkle.domain.base.enums.OrgTypeEnum;
import com.ulinkle.domain.base.vo.BaseOrgVo;
import com.ulinkle.domain.common.SystemField;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.open.api.impl.WxOpenOAuth2ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.NotBlank;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 微信扫码登录控制器(总部端)
 * 处理微信授权登录、回调处理、账号绑定等流程
 */
@RestController
@RequestMapping("/wx/head/login")
@Api(tags = "扫码登录: 总部", description = "总部系统微信扫码登录相关接口")
@AllArgsConstructor
@Slf4j
@Validated
public class LoginWxHeadController {

    private static final String OPEN_WX_LOGIN_KEY = "open:wx_login:";

    private final WxOpenOAuth2ServiceImpl wxOpenOAuth2Service;

    private final WxOpenConfig wxOpenConfig;  // 注入配置类获取回调地址

    private RedisTemplate<String, String> redisTemplate;

    private final BaseStaffService baseStaffService;

    private final LoginService loginService;

    private final BaseOrgService baseOrgService;


    /**
     * 1.获取微信登录二维码URL(前端渲染二维码)
     */
    @GetMapping("/qrcode")
    public DataResponse<String> getLoginQrCode() {
        try {
            // 生成state参数(防CSRF,建议存储到Redis,设置过期时间)
            String state = RandomUtil.randomString(32);


            redisTemplate.opsForValue().set(OPEN_WX_LOGIN_KEY + state, state, Constants.OPEN_QR_EXPIRE_FIVE, TimeUnit.MINUTES);
            // 拼接授权URL(WxJava工具包已封装拼接逻辑)
            String authorizationUrl = wxOpenOAuth2Service.buildAuthorizationUrl(wxOpenConfig.getRedirectUri(), wxOpenConfig.getScope(), state);
            log.info("生成二维码state:{}", state);
            return DataResponse.builderSuccess(StrPool.EMPTY, authorizationUrl);
        } catch (Exception e) {
            log.error("获取微信登录二维码URL失败:{}", e.getMessage());
            throw new CheckedException("获取微信登录二维码URL失败");
        }
    }


    /**
     * 2.处理微信回调,获取用户信息
     *
     * @param code  微信返回的授权码
     * @param state 微信返回的state参数(需校验)
     * @return 登录结果(包含openid、用户信息、自有令牌等)
     * @throws Exception 异常
     */
    @GetMapping("/callback")
    @ApiOperation(value = "2.小程序绑定手机号和openId,用户必须先在系统等级并填写手机号,然后通过本接口绑定手机号和openId")
    public DataResponse<UserInfoVo> handleCallback(@RequestParam("code") String code,
                                                   @RequestParam("state") String state) throws Exception {
        log.info("微信回调处理开始,code:{}, state:{}", code, state);

        //1. 校验state(防CSRF)
        String storedState = redisTemplate.opsForValue().get(OPEN_WX_LOGIN_KEY + state);
        if (storedState == null || !storedState.equals(state)) {
            throw new CheckedException("非法的state参数");
        }
        redisTemplate.delete(OPEN_WX_LOGIN_KEY + state);

        // 2. 通过code获取access_token和openid
        WxOAuth2AccessToken accessToken = wxOpenOAuth2Service.getAccessToken(code);
        String openId = accessToken.getOpenId();
        Optional<BaseStaff> staffOptional = baseStaffService.getByOpenId(openId);
        BaseStaff baseStaff = staffOptional.orElse(null);

        UserInfoVo result = BeanPlusUtil.toBean(wxOpenOAuth2Service.getUserInfo(accessToken, null), UserInfoVo.class);
        if (baseStaff != null && StringUtils.isNotEmpty(baseStaff.getTelephone())) {
            ArgumentAssert.equals(baseStaff.getState(), SystemField.StateEnum.NORMAL.getKey(), "用户已被禁用!");

            BaseOrgVo orgVo = baseOrgService.details(baseStaff.getOrgId());
            ArgumentAssert.equals(OrgTypeEnum.DEPT.getId(), orgVo.getOrgType(), "只有总部用户才能登录!");

            String token = loginService.createToken(baseStaff);
            log.info("扫码登录成功,token:{},用户ID:{},用户名称:{}", token, baseStaff.getId(), baseStaff.getUserName());

            result.setToken(token);
        } else {
            // 3. 获取用户信息(可选,根据业务需要)
            result.setTampToken(openId);
            log.info("扫码登录失败,跳转绑定页面,UserInfoVo:{}", result);
        }
        return DataResponse.builderSuccess(result);
    }


    /**
     * 3.用户与openId绑定
     *
     * @return 登录结果(包含openid、用户信息、自有令牌等)
     * @throws Exception 异常
     */
    @PostMapping("/bind")
    @ApiOperation(value = "2.用户绑定openId")
    public DataResponse<String> phone(@ApiParam(name = "ao", value = "参数不能为空", required = true)
                                      @RequestBody UserInfoVo ao) {
        log.info("用户与openid绑定,UserInfoVo:{}", ao);

        //1.校验OpenId是否被绑定
        String openId = ao.getTampToken();
        Optional<BaseStaff> staffOptional = baseStaffService.getByOpenId(openId);
        BaseStaff openIdStaff = staffOptional.orElse(null);

        ArgumentAssert.isNull(openIdStaff, "该OpenId已绑定用户!");

        //2.校验用户密码正确性
        BaseStaff baseStaff = loginService.checkPasswordAndGet(ao.getUserName(), ao.getPassword());

        //3.校验用户是否绑定其他openId
        String oldOpenId = baseStaff.getOpenId();
        ArgumentAssert.isTrue(StrUtil.isBlank(oldOpenId), "该用户已绑定OpenId!");
        baseStaff.setOpenId(openId);

        //4.绑定
        BaseStaff updateStaff = new BaseStaff();
        updateStaff.setId(baseStaff.getId());
        updateStaff.setOpenId(openId);
        boolean b = this.baseStaffService.updateById(updateStaff);
        ArgumentAssert.isTrue(b, "该用户已绑定OpenId失败!");

        //5.生成token
        String token = loginService.createToken(baseStaff);

        log.info("用户与openid绑定成功,username:{},openId:{}", ao.getUserName(), openId);

        return DataResponse.builderSuccess(StrPool.EMPTY, token);
    }
}


/**
 * 微信用户信息
 */
@Data
class UserInfoVo {
    /**
     * nickname	普通用户昵称
     */
    @ApiModelProperty("nickname	普通用户昵称")
    private String nickname;
    /**
     * sex	普通用户性别,1为男性,2为女性
     */
    @ApiModelProperty("sex	普通用户性别,1为男性,2为女性")
    private Integer sex;
    /**
     * city	普通用户个人资料填写的城市
     */
    @ApiModelProperty("city	普通用户个人资料填写的城市")
    private String city;

    /**
     * province	普通用户个人资料填写的省份
     */
    @ApiModelProperty("province	普通用户个人资料填写的省份")
    private String province;
    /**
     * country	国家,如中国为CN
     */
    @ApiModelProperty("country	国家,如中国为CN")
    private String country;
    /**
     * headimgurl	用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),
     * 用户没有头像时该项为空
     */
    @SerializedName("headimgurl")
    @ApiModelProperty("用户头像")
    private String headImgUrl;

    /**
     * openId
     */
    @ApiModelProperty("未绑定openId的token")
    @NotBlank(message = "临时token不能为空")
    private String tampToken;

    /**
     * token
     */
    @ApiModelProperty("登录成功token")
    private String token;

    /**
     * 用户名
     */
    @ApiModelProperty("用户名")
    @NotBlank(message = "用户名不能为空")
    private String userName;

    /**
     * 用户密码
     */
    @ApiModelProperty("用户密码")
    @NotBlank(message = "用户密码不能为空")
    private String password;

}

3. 其他

3.1 login相关的业务类和user相关的业务实体忽略

3.2 调试回调地址使用花生壳

3.3 与一般扫码登录流程有区别,多了一个绑定的不步骤

一般流程:openid没找到用户时,直接创建一个;
区别:现有系统用户是需要管理员新增的,所以这里多了一个绑定的动作

4. 少了轮训接口

二、网页内置二维码的方式

微信官方文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
在这里插入图片描述

1.后端代码


import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.ulinkle.base.api.BaseOrgService;
import com.ulinkle.base.api.BaseStaffService;
import com.ulinkle.base.api.LoginService;
import com.ulinkle.base.config.WxOpenConfig;
import com.ulinkle.base.controller.wx.vo.UserInfoVo;
import com.ulinkle.common.core.constant.Constants;
import com.ulinkle.common.core.exception.CheckedException;
import com.ulinkle.common.core.model.cache.CacheKey;
import com.ulinkle.common.core.utils.ArgumentAssert;
import com.ulinkle.common.core.utils.BeanPlusUtil;
import com.ulinkle.common.core.utils.StrPool;
import com.ulinkle.common.core.web.page.DataResponse;
import com.ulinkle.common.redis.cache.CachePlusOps;
import com.ulinkle.common.redis.cache.CacheResult;
import com.ulinkle.domain.base.entity.BaseStaff;
import com.ulinkle.domain.base.enums.OrgTypeEnum;
import com.ulinkle.domain.base.vo.BaseOrgVo;
import com.ulinkle.domain.common.SystemField;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.open.api.impl.WxOpenOAuth2ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 微信扫码登录控制器(总部端)
 * 处理微信授权登录、回调处理、账号绑定等流程
 */
@RestController
@RequestMapping("/wx/head/login")
@Api(tags = "扫码登录: 总部", description = "总部系统微信扫码登录相关接口")
@AllArgsConstructor
@Slf4j
@Validated
public class LoginWxHeadController {

    private static final String OPEN_WX_LOGIN_KEY = "open:wx_login:";

    private static final String OPEN_WX_USER_INFO_KEY = "open:wx_user_info:%s";

    private final WxOpenOAuth2ServiceImpl wxOpenOAuth2Service;

    private final WxOpenConfig wxOpenConfig;  // 注入配置类获取回调地址

    private RedisTemplate<String, Object> redisTemplate;

    private final CachePlusOps cachePlusOps;



    private final BaseStaffService baseStaffService;

    private final LoginService loginService;

    private final BaseOrgService baseOrgService;

//
//    /**
//     * 1.获取微信登录二维码URL(前端渲染二维码)
//     */
//    @GetMapping("/qrcode")
//    public DataResponse<String> getLoginQrCode() {
//        try {
//            // 生成state参数(防CSRF,建议存储到Redis,设置过期时间)
//            String state = RandomUtil.randomString(32);
//
//
//            redisTemplate.opsForValue().set(OPEN_WX_LOGIN_KEY + state, state, Constants.OPEN_QR_EXPIRE_FIVE, TimeUnit.MINUTES);
//            // 拼接授权URL(WxJava工具包已封装拼接逻辑)
//            String authorizationUrl = wxOpenOAuth2Service.buildAuthorizationUrl(wxOpenConfig.getRedirectUri(), wxOpenConfig.getScope(), state);
//            log.info("生成二维码state:{}", state);
//            return DataResponse.builderSuccess(StrPool.EMPTY, authorizationUrl);
//        } catch (Exception e) {
//            log.error("获取微信登录二维码URL失败:{}", e.getMessage());
//            throw new CheckedException("获取微信登录二维码URL失败");
//        }
//    }

    /**
     * 1.获取微信登录二维码URL(前端渲染二维码)
     */
    @GetMapping("/qrcodeState")
    public DataResponse<String> qrcodeState() {
        try {
            // 生成state参数(防CSRF,建议存储到Redis,设置过期时间)
            String state = RandomUtil.randomString(32);

            redisTemplate.opsForValue().set(OPEN_WX_LOGIN_KEY + state, state, Constants.OPEN_QR_EXPIRE_FIVE, TimeUnit.MINUTES);
            // 拼接授权URL(WxJava工具包已封装拼接逻辑)
            log.info("获取state:{}", state);
            return DataResponse.builderSuccess(StrPool.EMPTY, state);
        } catch (Exception e) {
            log.error("获取state:{}", e.getMessage());
            throw new CheckedException("获取获取state失败");
        }
    }


    /**
     * 2 .微信授权回调处理
     * @param code
     * @param state
     * @return
     * @throws Exception
     */
    @GetMapping("/callback")
    @ApiOperation(value = "2.微信授权回调处理", notes = "获取用户信息并缓存,供前端轮询获取")
    public DataResponse<UserInfoVo> handleCallback(@RequestParam("code") String code,
                                                   @RequestParam("state") String state) throws Exception {
        log.info("微信回调处理开始,code:{}, state:{}", code, state);

        // 1. 校验state(防CSRF)
        String storedState = (String) redisTemplate.opsForValue().get(OPEN_WX_LOGIN_KEY + state);
        if (storedState == null || !storedState.equals(state)) {
            throw new CheckedException("非法的state参数");
        }

        // 2. 通过code获取access_token和openid
        WxOAuth2AccessToken accessToken = wxOpenOAuth2Service.getAccessToken(code);
        String openId = accessToken.getOpenId();
        Optional<BaseStaff> staffOptional = baseStaffService.getByOpenId(openId);
        BaseStaff baseStaff = staffOptional.orElse(null);

        // 3. 构建用户信息VO
        UserInfoVo result = BeanPlusUtil.toBean(wxOpenOAuth2Service.getUserInfo(accessToken, null), UserInfoVo.class);
        if (baseStaff != null && StringUtils.isNotEmpty(baseStaff.getTelephone())) {
            ArgumentAssert.equals(baseStaff.getState(), SystemField.StateEnum.NORMAL.getKey(), "用户已被禁用!");

            BaseOrgVo orgVo = baseOrgService.details(baseStaff.getOrgId());
            ArgumentAssert.equals(OrgTypeEnum.DEPT.getId(), orgVo.getOrgType(), "只有总部用户才能登录!");

            String token = loginService.createToken(baseStaff);
            log.info("扫码登录成功,token:{},用户ID:{},用户名称:{}", token, baseStaff.getId(), baseStaff.getUserName());

            result.setToken(token);
        } else {
            // 未绑定用户,设置临时token(openId)
            result.setTampToken(openId);
            log.info("扫码登录未绑定用户,跳转绑定页面,UserInfoVo:{}", result);
        }

        // 4. 缓存UserInfoVo对象(使用state作为key,设置过期时间)

        cachePlusOps.set(new CacheKey(String.format(OPEN_WX_USER_INFO_KEY, state), Duration.ofMinutes(Constants.OPEN_QR_EXPIRE_FIVE)) , result, false);
        log.info("用户信息已缓存,state:{}, UserInfoVo:{}", state, result);

        // 5. 删除state的防CSRF缓存(已完成回调校验)
        redisTemplate.delete(OPEN_WX_LOGIN_KEY + state);

        return DataResponse.builderSuccess(result);
    }

    public UserInfoVo getOrderCache(String state) {
        CacheResult<UserInfoVo> order = cachePlusOps.get(new CacheKey(String.format(OPEN_WX_USER_INFO_KEY, state)));
        return order.getValue();
    }

    /**
     * 3.新增:根据state轮询获取缓存的用户信息
     *
     * @param state 微信授权的state参数
     * @return 缓存的用户信息(未获取到返回空,前端继续轮询)
     */
    @GetMapping("/polling")
    @ApiOperation(value = "3.轮询获取用户登录信息", notes = "前端根据state轮询,获取缓存的用户信息,未获取到返回空")
    public DataResponse<UserInfoVo> pollingUserInfo(@ApiParam(name = "state", value = "二维码state参数", required = true)
                                                    @RequestParam("state") String state) {
        try {
            log.info("轮询获取用户信息,state:{}", state);

            // 从缓存获取用户信息
            UserInfoVo userInfoVo = this.getOrderCache(state);

            // 未获取到返回空,前端继续轮询;获取到则返回并删除缓存(避免重复获取)
            if (userInfoVo != null) {
                cachePlusOps.del(new CacheKey(String.format(OPEN_WX_USER_INFO_KEY, state)));
//                redisTemplate.delete(OPEN_WX_USER_INFO_KEY + state);
                log.info("轮询获取用户信息成功,state:{}, UserInfoVo:{}", state, userInfoVo);
                return DataResponse.builderSuccess(userInfoVo);
            } else {
                log.info("轮询未获取到用户信息,state:{}", state);
                return DataResponse.builderSuccess(null); // 返回空,前端继续轮询
            }
        } catch (Exception e) {
            log.error("轮询获取用户信息失败,state:{}, 异常:{}", state, e.getMessage());
            throw new CheckedException("轮询获取用户信息失败");
        }
    }


    /**
     * 4.用户与openId绑定
     *
     * @return 登录结果(包含openid、用户信息、自有令牌等)
     * @throws Exception 异常
     */
    @PostMapping("/bind")
    @ApiOperation(value = "2.用户绑定openId")
    public DataResponse<String> phone(@ApiParam(name = "ao", value = "参数不能为空", required = true)
                                      @RequestBody UserInfoVo ao) {
        log.info("用户与openid绑定,UserInfoVo:{}", ao);

        //1.校验OpenId是否被绑定
        String openId = ao.getTampToken();
        Optional<BaseStaff> staffOptional = baseStaffService.getByOpenId(openId);
        BaseStaff openIdStaff = staffOptional.orElse(null);

        ArgumentAssert.isNull(openIdStaff, "该OpenId已绑定用户!");

        //2.校验用户密码正确性
        BaseStaff baseStaff = loginService.checkPasswordAndGet(ao.getUserName(), ao.getPassword());

        //3.校验用户是否绑定其他openId
        String oldOpenId = baseStaff.getOpenId();
        ArgumentAssert.isTrue(StrUtil.isBlank(oldOpenId), "该用户已绑定OpenId!");
        baseStaff.setOpenId(openId);

        //4.绑定
        BaseStaff updateStaff = new BaseStaff();
        updateStaff.setId(baseStaff.getId());
        updateStaff.setOpenId(openId);
        boolean b = this.baseStaffService.updateById(updateStaff);
        ArgumentAssert.isTrue(b, "该用户已绑定OpenId失败!");

        //5.生成token
        String token = loginService.createToken(baseStaff);

        log.info("用户与openid绑定成功,username:{},openId:{}", ao.getUserName(), openId);

        return DataResponse.builderSuccess(StrPool.EMPTY, token);
    }
}



2.前端代码:略

前端配置appid和回调url地址;
生成二维码的state需要后台获取,然后生成二维码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值