网站实现微信扫码登录
1 准备工作
1.1 申请网站应用
在接入微信扫码登录之前,在微信开放平台注册开发者账号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。
找到回调授权域名这一项,修改成要回调的域名,例如:www.baidu.com 不要加http://或https://。
2 快速开始
2.1 微信扫码登录流程说明
1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
简单来说,我们用手机微信扫描网站应用上的微信登录二维码,会弹出是否允许
或拒绝
按钮,当我们点击允许
按钮时,微信会重定向到授权回调域
地址上。若用户禁止授权,则不会发生重定向。授权回调域映射的是我们网站应用后端接口,来处理相关登录逻辑。
2.2 生成微信二维码的两种方式
2.2.1 微信提供的二维码生成URL
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 应用唯一标识 |
redirect_uri | 是 | 请使用urlEncode对链接进行处理 |
response_type | 是 | 填code |
scope | 是 | 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login |
state | 否 | 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验 |
lang | 否 | 界面语言,支持cn(中文简体)与en(英文),默认为cn |
正确填写微信提供的二维码生成URL参数后,在浏览器上访问会生成二维码。例如:
2.2.2 将微信登录二维码内嵌到自己页面
步骤1:在页面中先引入如下JS文件(支持https)
http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
步骤2:在需要使用微信登录的地方实例以下JS对象:
var obj = new WxLogin({
self_redirect:true,
id:"login_container",
appid: "",
scope: "",
redirect_uri: "",
state: "",
style: "",
href: ""
});
参数说明:
参数 | 是否必须 | 说明 |
---|---|---|
self_redirect | 否 | true:手机点击确认登录后可以在 iframe 内跳转到 redirect_uri,false:手机点击确认登录后可以在 top window 跳转到 redirect_uri。默认为 false |
id | 是 | 第三方页面显示二维码的容器id |
appid | 是 | 应用唯一标识,在微信开放平台提交应用审核通过后获得 |
scope | 是 | 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可 |
redirect_uri | 是 | 重定向地址,需要进行UrlEncode |
state | 否 | 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验 |
style | 否 | 提供"black"、"white"可选,默认为黑色文字描述。 |
href | 否 | 自定义样式链接,第三方可根据实际需求覆盖默认样式。 |
2.3 系统微信扫码登录示例
2.3.1 流程说明
① 对于我们系统来说,微信扫码登录的本质就是将自己系统用户与微信用户进行绑定,我这里是通过微信openid
进行绑定。
② 用户扫描微信二维码允许登录,微信会重定向到授权回调域,并且带上code
和state
参数,授权回调域与/user/callback
接口进行映射。
③ /user/callback
接口处理时,通过appId
、appSecret
、code
参数去调用 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
接口,调用接口的凭证,有了它可以获取里面的openid等信息。
④ 通过openid
去自己系统数据库查询用户,如果查到用户,就允许微信登录成功,查不到用户,就微信登录失败。
⑤ 关于扫码登录成功跳转系统首页,可以在/user/callback
接口中直接发起页面重定向。这种重定向适合简单业务的微信扫码登录,复杂的就不适用。
2.3.2 核心代码
2.3.2.1 前端代码
<div *ngIf="loginWayFlag" class="login-WX">
<div id="login_container"></div>
</div>
initWxLogin () {
new WxLogin({
self_redirect: true,
id: 'login_container',
appid: 'xxx',
scope: 'snsapi_login',
redirect_uri: encodeURIComponent('https://xxx.com/'),
state: '@platformId@:@' + this.user.platformId + '@,@systemId@:@' + this.user.systemId + '@,@scanType@:@login@,@wxRandomId@:@' + this.user.wxRandomId + '@',
style: 'black',
href: 'data:text/css;base64,IC5pbXBvd2VyQm94IC5xcmNvZGUge3dpZHRoOiAxNTBweDt9Ci5pbXBvd2VyQm94IC50aXRsZSB7ZGlzcGxheTogbm9uZTt9Ci5zdGF0dXNfaWNvbiB7ZGlzcGxheTogbm9uZX0KLmltcG93ZXJCb3ggLnN0YXR1cyB7dGV4dC1hbGlnbjogY2VudGVyO30KLmltcG93ZXJCb3ggLmljb24zOF9tc2cuc3VjY3t3aWR0aDogMzBweDtoZWlnaHQ6IDMwcHg7ZGlzcGxheTogYmxvY2s7bWFyZ2luLWxlZnQ6IDExMXB4O30KLmltcG93ZXJCb3ggLmljb24zOF9tc2cud2Fybnt3aWR0aDogMzBweDtoZWlnaHQ6IDMwcHg7ZGlzcGxheTogYmxvY2s7bWFyZ2luLWxlZnQ6IDExMXB4O30KaDEsIGgyLCBoMywgaDQsIGg1LCBoNiwgcHtmb250LXNpemU6IDE0cHg7fQ==',
});
}
2.3.2.2 后端代码
微信常量类
/**
* 微信常量类
*/
public class WxConstant {
/**
* APPID
*/
public static final String APP_ID = "xxx";
/**
* APP密钥
*/
public static final String APP_SECRET = "xxx";
/**
* 授权回调域
*/
public static final String CALLBACK_SERVER = "http://xxx.com";
/**
* 获取二维码接口URL
*/
public static final String QRCODE_URL = "https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect";
/**
* 获取access_token和openid接口URL
*/
public static final String OPENID_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
}
微信工具类
/**
* 微信工具类
*/
@Log4j2
public class WxUtil {
/**
* 用于将扫描二维码后重定向的资源url进行编码
*/
public static String urlEncode(String source){
String result = source;
try {
result = java.net.URLEncoder.encode(source,"utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
/**
* 根据code、appid和secret获取access_token(也就是调用接口的凭证,有了它可以获取里面的openid等信息)
*/
public static WxTokenDTO getTokenWithOpenid(String appId, String appSecret, String code) throws Exception {
String baseUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
String requestUrl = baseUrl.replace("APPID", appId).replace("SECRET", appSecret).replace("CODE", code);
// 发起GET请求获取凭证
String result = GetUtil.doGet(requestUrl);
JSONObject jsonObject = JSON.parseObject(result);
WxTokenDTO token = new WxTokenDTO();;
token.setOpenId(jsonObject.getString("openid"));
token.setAccessToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getIntValue("expires_in"));
return token;
}
/**
* 根据access_token和openid获取微信用户信息
*/
public static WxUserDTO getWeChatUserInfo(String access_token, String openid) throws Exception {
String baseUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
String requestUrl = baseUrl.replace("ACCESS_TOKEN", access_token).replace("OPENID", openid);
String result = GetUtil.doGet(requestUrl);
JSONObject jsonObject = JSON.parseObject(result);
WxUserDTO userDTO = new WxUserDTO();
userDTO.setNickname(jsonObject.getString("nickname"));
userDTO.setHeadImgUrl(jsonObject.getString("headimgurl"));
userDTO.setUnionId(jsonObject.getString("unionid"));
return userDTO;
}
}
用户控制器类
授权回调域与/user/callback接口进行映射,code和state参数是微信传递过来的,state参数为自定义参数,我们可以通过它进行传递想要的参数。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/callback")
public RequestResult callback(String code, String state) throws Exception {
return userService.callback(code, state);
}
}
用户服务类
@Override
public RequestResult callback(String code, String state) throws Exception {
JSONObject jsonObject = new JSONObject();
JSONObject params = JSON.parseObject(String.format("{%s}", state.replaceAll("@", "\"")));
WxTokenDTO tokenDTO = WxUtil.getTokenWithOpenid(WxConstant.APP_ID, WxConstant.APP_SECRET, code);
log.info("[获取微信openId信息]:" + tokenDTO);
if ("login".equals(params.getString("scanType"))) {
wxLogin(tokenDTO.getOpenId(), params.getString("wxRandomId"), params.getLong("systemId"), jsonObject);
}
if ("bindUserOpenId".equals(params.getString("scanType"))) {
bindUserOpenId(params.getLong("userId"), tokenDTO.getOpenId(), params.getLong("systemId"), jsonObject);
}
return RequestResult.success(jsonObject);
}
/**
* 微信扫码登录
*/
private void wxLogin(String openId, String wxRandomId, Long systemId, JSONObject resultObject) {
UserDTO userDTO = baseMapper.getByOpenId(openId, systemId);
if (ObjectUtil.isEmpty(userDTO)) {
resultObject.put("loginResult", getLoginFailHtml());
return;
}
int result = baseMapper.updateUserLoginWxRandomId(userDTO.getId(), wxRandomId);
if (result > 0) {
resultObject.put("loginResult", getLoginSuccessHtml());
} else {
resultObject.put("loginResult", getLoginFailHtml());
}
}
/**
* 添加用户信息后,绑定微信openId
*/
private void bindUserOpenId(Long userId, String openId, Long systemId, JSONObject jsonObject) {
UserDTO findUserDTO = baseMapper.getByOpenId(openId, systemId);
if (ObjectUtil.isNotEmpty(findUserDTO)) {
jsonObject.put("bindUserOpenIdResult", getAlreadyBindOpenIdHtml());
return;
}
User user = new User();
user.setId(userId);
user.setOpenId(openId);
if (baseMapper.updateById(user) > 0) {
jsonObject.put("bindUserOpenIdResult", getBindOpenIdSuccessHtml());
} else {
jsonObject.put("bindUserOpenIdResult", getBindOpenIdFailHtml());
}
}