目录
- 第一步:生成微信扫描二维码(网页内嵌的二维码)接口
- 第二步:处理微信回调 ,获取扫描人信息
微信开放平台:
https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
第一步:生成微信扫描二维码(网页内嵌的二维码)接口
提供了第二种获取 code 的方式,支持网站将微信登录二维码内嵌到自己页面中,用户使用微信扫码授权后通过 JS 将code返回给网站。 JS微信登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。 网站内嵌二维码微信登录 JS 实现办法:
步骤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: ""
});
参数说明
我们需要写必须的那几个参数;
生成微信扫描二维码(网页内嵌的二维码)接口.此几口返回的数据是:生成二维码需要的参数
注意:接口的后台需要准备这几个参数:
appid : 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope :应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
redirect_uri : 重定向地址,需要进行UrlEncode
state : 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止 csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加 session 进行校验
1.需要在显示二维码的页面 引入前端api接口:
import weixinApi from ‘@/api/weixin’
2.然后在 mounted() 方法中添加以下方法
mounted() {
//初始化微信js
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
document.body.appendChild(script)
},
3、实例化微信JS对象
添加微信登录方法,即点击微信登录按钮,触发展示微信二维码的方法:
weixinLogin() {
this.dialogAtrr.showLoginType = 'weixin'
weixinApi.getLoginParam().then(response => {
var obj = new WxLogin({
self_redirect:true,
id: 'weixinLogin', // 需要显示的容器id
appid: response.data.appid, // 公众号appid wx*******
scope: response.data.scope, // 网页默认即可
redirect_uri: response.data.redirectUri, // 授权成功后回调的url
state: response.data.state, // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css文件url,需要https
})
})
},
然后记得网关设置:
测试二维码展示:
第二步:处理微信回调 ,获取扫描人信息
手机点击确认后的:
请求网址: http://localhost:8160/api/ucenter/wx/callback?code=0815s6ll2O8vK94DBoll2u3htf45s6lP&state=1661166686989
请求网址: http://localhost:8160/api/ucenter/wx/callback?code=091927ll2UTvK94eW6nl2segEs3927lr&state=1661167265572
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
四、令牌与密码
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
- 客户应用请求授权服务器获取Access Token
- 授权服务器咨询用户意见
- 用户同意授权
- 授权服务器颁发Access Token 给 客户应用
创建工具类:httpclint:
在server_user模块:
在common-util中引入httpclient依赖:
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
第二步:通过 code 获取access_token
通过 code 获取access_token
https://api.weixin.qq.com/sns/oauth2/
access_token?appid=APPID
&secret=SECRET&
code=CODE&
grant_type=authorization_code
说明:我们根据返回openid判断是否需要绑定手机号码,如果需要绑定,那么我们要根据openid用户用户信息,然后更新上手机号码
前端:
3.4 回调返回页面
操作:yygh-site
说明:我们只期望返回一个空页面,然后跟登录层通信就可以了,其实就是一个过渡页面,所以我们要给这个过渡页面定义一个空模板
3.4.2回调返回页面
根据返回路径/weixin/cakkback,我们创建组件/weixin/cakkback.vue
说明:在页面我们就能够接收到返回来的参数
3.4.3 父组件定义回调方法
在myheader.vue添加方法
mounted() 中增加方法:
// 微信登录回调处理
let self = this;
window["loginCallback"] = (name,token, openid) => {
self.loginCallback(name, token, openid);
}
然后methods中增加方法:
loginCallback(name, token, openid) {
// 打开手机登录层,绑定手机号,改逻辑与手机登录一致
if(openid != '') {
this.userInfo.openid = openid
this.showLogin()
} else {
this.setCookies(name, token)
}
},
3.5 服务器绑定手机号码
页面绑定手机号码会把openid传递过来,我们根据openid找到用户信息,然后绑定手机号码
修改UserInfoServiceImpl类登录方法
//绑定手机号码
UserInfo userInfo = null;
if(!StringUtils.isEmpty(loginVo.getOpenid())) {
userInfo = this.selectWxInfoOpenId(loginVo.getOpenid());
if(null != userInfo) {
userInfo.setPhone(loginVo.getPhone());
this.updateById(userInfo);
} else {
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
}
if(userInfo == null){
//判断是否第一次登录,根据手机号查询数据库,如果不存在相同的手机号就是第一次登录
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("phone",phone);
userInfo = baseMapper.selectOne(wrapper);
if(null==userInfo ){//第一次使用这个手机号登录
//添加信息到数据库
userInfo = new UserInfo();
userInfo.setName("");
userInfo.setPhone(phone);
userInfo.setStatus(1);//设置可用
//this.save(userInfo);//保存到数据库
baseMapper.insert(userInfo);
}
}
package com.fan.yygh.user.controller;
import com.alibaba.fastjson.JSONObject;
import com.fan.yygh.common.exception.YyghException;
import com.fan.yygh.common.result.Result;
import com.fan.yygh.common.result.ResultCodeEnum;
import com.fan.yygh.helper.JwtHelper;
import com.fan.yygh.model.user.UserInfo;
import com.fan.yygh.user.service.UserInfoService;
import com.fan.yygh.user.utils.ConstantPropertiesUtil;
import com.fan.yygh.user.utils.HttpClientUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
//微信操作的接口
@Api("微信")
@Controller
@RequestMapping("/api/ucenter/wx")
public class WeixinApiController {
@Resource
private UserInfoService userInfoService;
/**
* 获取微信登录参数
* 1.生成微信扫描二维码
* 返回生成二维码需要的参数
*/
@GetMapping("getLoginParam")
@ResponseBody
public Result genQrConnect() throws UnsupportedEncodingException {
String redirect_uri = URLEncoder.encode(
ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL, "UTF-8");
Map<String, Object> map = new HashMap<>();
map.put("appid", ConstantPropertiesUtil.WX_OPEN_APP_ID);
map.put("redirect_uri", redirect_uri);
map.put("scope", "snsapi_login");
map.put("state", System.currentTimeMillis()+"");//System.currentTimeMillis()+""
return Result.ok(map);
}
//微信扫描后回调的方法
@GetMapping("callback")
public String callback(String code,String state){
//第一步,获取 临时授权临时票据code
System.out.println("code:"+code);
//第二步:拿着code和微信appid和秘钥appscrect(三个参数换一个参数),--》请求微信固定地址--》换取access_token
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/oauth2/access_token")
.append("?appid=%s")
.append("&secret=%s")
.append("&code=%s")
.append("&grant_type=authorization_code");
//baseAccessTokenUrl的参数填充
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantPropertiesUtil.WX_OPEN_APP_ID, //参数一appid
ConstantPropertiesUtil.WX_OPEN_APP_SECRET, //参数二appscrec
code); //参数三code
//使用httpclient请求这个地址
String accessTokenInfo = null;
try {
//使用三个参数换取token,accessTokenInfo包含token令牌信息
accessTokenInfo = HttpClientUtils.get(accessTokenUrl);//accessTokenUrl包含三个参数
/*令牌accessTokenInfo:
{"access_token":"60_jEgLorfJRsMwTfuDdzUN_7mApa4D7c",
"expires_in":7200,
"refresh_token":"60_LPZ7M5sigEJ_wB09HKCeMETuYwLBjg",
"openid":"o3_SC5yAwrC4BO587_u3_llgcHZU",
"scope":"snsapi_login",
"unionid":"oWgGz1KqBehsbtgjrIjCR8-5hxkU"}*/
System.out.println("使用code换取的access_token结果accessTokenInfo:"+accessTokenInfo);
/*=============================以下是对换取的token做处理=======================================*/
//从返回字符串拿到两个值openid 和 access_token
JSONObject jsonObject = JSONObject.parseObject(accessTokenInfo);
String access_token = jsonObject.getString("access_token");
String openid = jsonObject.getString("openid");
//判断数据库是否存在微信的扫描人信息,根据openid判断
UserInfo userInfo = userInfoService.selectWxInfoOpenId(openid);
if(userInfo == null){ //数据库不存在微信信息
//第三步:拿着openid 和access_token 请求微信地址,得到扫码人 信息
//根据access_token获取微信用户的基本信息
//先根据openid进行数据库查询
// UserInfo userInfo = userInfoService.getByOpenid(openId);
// 如果没有查到用户信息,那么调用微信个人信息获取的接口
// if(null == userInfo){
//如果查询到个人信息,那么直接进行登录
//使用access_token换取受保护的资源:微信的个人信息
//换取token的地址: https://api.weixin.qq.com/sns/oauth2/access_token
//换取user信息的地址:https://api.weixin.qq.com/sns/userinfo
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
String resultInfo = HttpClientUtils.get(userInfoUrl);
//得到扫码 用户信息resultInfo,是一个json字符串
/*扫码人信息resultInfo:
{"openid":"o3_SC5yAwrC4BO587_u3_llgcHZUxxx",
"nickname":"xxx","sex":0,"language":"","city":"","province":"","country":"",
"headimgurl":"https:\23\/132",
"privilege":[],
"unionid":"oWgGz1KqBehsbtgjrIjCR8-5hxkU"}*/
//================第四步:利用access_token 和openid 换取 扫码人信息=============
System.out.println("扫码人信息resultInfo:"+resultInfo);
//解析用户信息,将string转成json对象
JSONObject resultUserInfoJson = JSONObject.parseObject(resultInfo);
String nickname = resultUserInfoJson.getString("nickname");//得到用户昵称
String headimgurl = resultUserInfoJson.getString("headimgurl");//得到用户头像
//获取扫码人信息 并添加到数据库
userInfo = new UserInfo();
userInfo.setName(nickname);
userInfo.setOpenid(openid);
userInfo.setStatus(1);
//保存到数据库
userInfoService.save(userInfo);
}
//返回name和token令牌字符串
HashMap<String, String> map = new HashMap<>();
String name = userInfo.getName();
if(StringUtils.isEmpty(name)){
name = userInfo.getNickName();
}
if(StringUtils.isEmpty(name)){
name = userInfo.getPhone();
}
map.put("name",name);//将名字放入map
//要求每个微信用户要绑定手机号,判断userInfo是否有手机号,
// 如果手机号为空,返回openid
//如果手机号不为空,返回openid值是空字符串
//前端判断:如果openid不为空,绑定手机号,如果openid为空,不需要绑定手机号
if(StringUtils.isEmpty(userInfo.getPhone())){
map.put("openid", userInfo.getOpenid());
}else{
map.put("openid","");
}
//产生token令牌字符串
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token",token);//将令牌token放入map
//登录成功后的页面跳转
return "redirect:"+ ConstantPropertiesUtil.YYGH_BASE_URL + "/weixin/callback?token="+
map.get("token")+"&openid="+
map.get("openid")+"&name="+URLEncoder.encode(map.get("name"),"utf-8");
} catch (Exception e) {
throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
//return null;
}
}
/*============我的微信二维码生成的方法====================*/
/*方法返回生成展示二维码的各种前端页面参数*/
@ApiOperation("我的微信二维码生成的方法")
@GetMapping("createWeixinImage")
@ResponseBody //此注解返回json
public Result createWeixinImage() throws UnsupportedEncodingException {
//微信官方规定:redirect_uri 重定向地址,需要进行UrlEncode
String redirect_uri = URLEncoder.encode(
ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL,
"UTF-8");
HashMap<String, Object> map = new HashMap<>();
//从官网给的参数中获取appid,appid在配置文件中的
map.put("appid",ConstantPropertiesUtil.WX_OPEN_APP_ID);
//scope:应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即可
map.put("scope","snsapi_login");
map.put("redirect_uri",redirect_uri);
/*用于保持请求和回调的状态,授权请求后原样带回给第三方。
该参数可用于防止 csrf 攻击(跨站请求伪造攻击),
建议第三方带上该参数,可设置为简单的随机数加 session 进行校验*/
map.put("state",System.currentTimeMillis()+"");
return Result.ok(map);
}
}
package com.fan.yygh.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fan.yygh.common.exception.YyghException;
import com.fan.yygh.common.result.ResultCodeEnum;
import com.fan.yygh.helper.JwtHelper;
import com.fan.yygh.model.user.UserInfo;
import com.fan.yygh.user.mapper.UserInfoMapper;
import com.fan.yygh.user.service.UserInfoService;
import com.fan.yygh.vo.user.LoginVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Service //先继承《M是mapper,T是实体类》后实现
public class UserInfoImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService {
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private RedisTemplate<String,String> redisTemplate;
//判断数据库是否存在微信的扫描人信息,根据openid判断
@Override
public UserInfo selectWxInfoOpenId(String openid) {
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("openid",openid);
UserInfo userInfo = baseMapper.selectOne(wrapper);
return userInfo;
}
//用户手机号登录接口
@Override
public Map<String, Object> loginUser(LoginVo loginVo) {
//从vo中获取参数
String phone = loginVo.getPhone();
String code = loginVo.getCode();
//参数校验
if(StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)){
throw new YyghException(ResultCodeEnum.PARAM_ERROR);//ResultCodeEnum枚举类
}
//TODO 校验校验验证码
//校验校验验证码
String mobleCode = redisTemplate.opsForValue().get(phone);
if(!code.equals(mobleCode)) {
throw new YyghException(ResultCodeEnum.CODE_ERROR);
}
//绑定手机号码
UserInfo userInfo = null;
if(!StringUtils.isEmpty(loginVo.getOpenid())) {
userInfo = this.selectWxInfoOpenId(loginVo.getOpenid());
if(null != userInfo) {
userInfo.setPhone(loginVo.getPhone());
this.updateById(userInfo);
} else {
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
}
if(userInfo == null){
//判断是否第一次登录,根据手机号查询数据库,如果不存在相同的手机号就是第一次登录
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("phone",phone);
userInfo = baseMapper.selectOne(wrapper);
if(null==userInfo ){//第一次使用这个手机号登录
//添加信息到数据库
userInfo = new UserInfo();
userInfo.setName("");
userInfo.setPhone(phone);
userInfo.setStatus(1);//设置可用
//this.save(userInfo);//保存到数据库
baseMapper.insert(userInfo);
}
}
//校验是否被禁用
if(userInfo.getStatus() == 0) {
throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);
}
//返回给页面的map信息
HashMap<String, Object> map = new HashMap<>();
String name = userInfo.getName();
if (StringUtils.isEmpty(name)) {//如果名字为空
name = userInfo.getNickName();//用昵称代替名字
}
if(StringUtils.isEmpty(name)){
name = userInfo.getPhone();//如果名字为空,用手机号代替名字
}
map.put("name",name);//返回name和token
//利用工具生成token
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token",token);//token 被写错了
return map; //记得返回map
}
}