文章目录
一、微信外置链接二维码的方式
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需要后台获取,然后生成二维码
,两种方式&spm=1001.2101.3001.5002&articleId=155678457&d=1&t=3&u=aba2e03fd736401abeae02d55909c389)
3万+

被折叠的 条评论
为什么被折叠?



