谷粒学院16万字笔记+1600张配图(十五)——微信扫码登录
项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
文章目录
- demo15-微信扫码登录
- 1.OAuth2
- 1.1OAuth2介绍
- 1.2开放系统间授权
- 1.3分布式的访问问题
- 1.4OAuth2误解
- 2.准备工作
- 2.1注册
- 2.2申请网站应用名称
- 2.3需要域名地址
- 2.4老师已提供
- 2.5配置微信id、秘钥、域名地址
- 3.生成二维码
- 3.1看帮助文档
- 3.2控制层
- 3.3测试
- 3.4扫码成功后跳转规则
- 4.获取扫描人信息
- 4.1分析
- 4.2添加依赖
- 4.3创建httpclient工具类
- 4.4控制层
- 4.5业务层接口
- 4.6业务层实现类
- 4.7控制层功能完善
- 5.首页显示信息
- 5.1分析
- 5.2控制层整合jwt令牌
- 5.3获取首页路径中的token值
- 5.4将token值存入cookie
- 5.5根据token获取用户信息
- 5.6将获取到的用户信息存入cookie
- 6.测试
demo15-微信扫码登录
1.OAuth2
1.1OAuth2介绍
OAouth2是什么:OAouth2是针对特点问题的一种解决方案
OAouth2主要可以解决两个问题:
- 开放系统间授权
- 分布式的访问问题
1.2开放系统间授权
1.有一个叫lucy的人,她拍了很多照片并将其存储到了百度网盘,现在网络上有一个打印照片的服务,此时lucy想把存到百度网盘的照片交给这个服务去打印,可是现在有一个问题:lucy本人当然可以去访问操作百度网盘,但是打印照片服务并不能访问操作百度网盘,那么lucy必须给打印照片服务予以访问操作百度网盘的授权。OAuth2就是解决这个问题的,OAouth2用来规定该怎么授权,用什么方式授权,使得打印照片服务可以访问操作百度网盘,这个过程就叫做开放系统间授权
2.开放系统间授权的三种方式:
- lucy将百度网盘的账号密码提供给打印照片服务
- 不建议这种方式,安全性太低了
- 通用开发者key(万能钥匙)
- 打印照片服务和百度网盘商量出一个通用的钥匙,使用这把钥匙既可以打开百度网盘又可以打开打印照片服务
- 这种方式有一个缺陷:比如我创建了一个三个人的公司,想去找百度合作商量出一个通用的钥匙,人家肯定不会和我这三人小公司合作对吧
- 办法令牌
- 百度网盘给打印照片服务颁发一个令牌,OAuth2就是做这个事情。其实和我们在"demo14-注册、登录"的"1.2.4token(令牌机制)"说的token很像很像
- 既然颁发令牌那就有限制:要指定给谁颁发,还要对令牌进行管理:规定令牌多久有效,当然,也可以吊销令牌
1.3分布式的访问问题
1.分布式的访问问题指的就是我们在"demo14-注册、登录"的"1.用户登录业务介绍(单点登录)"中说的单点登录
2.比如说我登录了在线教育的edu模块后,然后访问vod模块和msm模块时就不再需要登录了,我们在"demo14-注册、登录"的"1.2.1单点登录三种常见方式"说的第三种方案(token)其实就是OAuth2方案,再复习一下token方案:
- 登录成功之后,按照一定规则生成字符串,字符串包含用户信息
- 把生成的字符串通过路径传递,或者放到cookie中
- 后面再次发送请求时,都会带着这个字符串进行发送
- 我们获取到字符串,如果从字符串中获取到了用户信息那就说明此时用户已登录
3.所以我们就知道了OAuth2解决方案就是:按照一定规则生成字符串,字符串中包含用户信息,这种方式有个专业术语:令牌机制
4.OAuth2只是一种解决方案,只是规定了大的实现方式,并没有规定我们必须使用哪种规则生成字符串。我们在"demo14-注册、登录"的"5.4业务层实现类(登录功能)"中使用的是JWT规则
1.4OAuth2误解
OAuth2只是一种解决方案,并不是一个认证协议、并没有定义授权处理机制、并没有定义token格式、并没有定义加密方法、并不是单个协议…仅仅只是一种解决方案
2.准备工作
2.1注册
1.如果想用微信做相关操作,必须在微信平台上支付300元注册一个开发者资质(但目前只支持企业进行注册,不支持个人申请注册)
网址:https://open.weixin.qq.com/wxaopen/regist/index
2.注册之后人家会提供给我们一个微信id和微信秘钥,拿着这个id和秘钥才可以进行微信的相关操作
2.2申请网站应用名称
用户扫过你这个二维码后会显示扫的是什么应用,显示的这个东西就叫做应用名称
2.3需要域名地址
使用微信扫过二维码后,就会找域名地址并做跳转(必须是www.xxx.com这类的域名地址,本地地址是不行的)
2.4老师已提供
1.上面说的id、秘钥、域名地址老师都给我们提供了
id:wxed9954c01bb89b47
秘钥:a7482517235173ddb4083788de60b90e
域名地址:http://guli.shop/api/ucenter/wx/callback
2.但看评论区说这个域名地址不能用了,需要进行如下修改:(我不知道为啥这样修改,这不是访问本地去了吗?)
- 后面使用域名地址时使用http://localhost:8160/api/ucenter/wx/callback而不使用老师提供的http://guli.shop/api/ucenter/wx/callback
2.5配置微信id、秘钥、域名地址
1.因为我们现在做的是登录部分,所以我们将后端写在service_ucenter模块中
在application.properties配置文件中进行配置:
# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback
在"2.4老师已提供"的第2步说过了,这里的域名地址要使用http://localhost:8160/api/ucenter/wx/callback而不是使用老师提供的http://guli.shop/api/ucenter/wx/callback
2.在educenter包下创建包utils,然后在utils包下创建类ConstantWxUtils用于读取application.properties配置文件中的内容
@Component
public class ConstantWxUtils implements InitializingBean {
@Value("${wx.open.app_id}") //value注解用spring包里面的,别导错包
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
3.生成二维码
3.1看帮助文档
帮助文档https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html中说了,生成二维码需要访问https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
,并且人家给我们说明了应该携带什么样的参数
人家腾讯规定的,redirect_uri需要使用urlEncode进行编码
3.2控制层
在controller包下专门创建一个控制器WxApiController来做微信的相关操作
@CrossOrigin
@RestController
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
//生成微信扫描二维码
@GetMapping("login")
public String getWxCode() {
//1.url中的%s就相当于问号(?),代表占位符
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
//2.对redirect_url进行URLEncode编码
String redirect_url = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
try {
redirect_url = URLEncoder.encode(redirect_url, "utf-8");
} catch(Exception e) {
}
//3.给baseUrl中的占位符(%s)赋值
String url = String.format(
baseUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
ConstantWxUtils.WX_OPEN_REDIRECT_URL,
"atguigu" //老师说了state目前没啥用,只是给他传个值atguigu
); //方法的返回值就是完整的带有参数的url地址
//4.重定向到请求微信地址里面
return "redirect:" + url;
}
}
-
为什么方法返回值类型是String:以前我们都是在控制层返回数据给前端,所以控制器中的方法返回值都是R.ok()或R.ok().data(“xxx”,xxx)。但是我们这里并不是想要返回数据,而是想要生成二维码,也就是说返回值需要是"3.1看帮助文档"中说的字符串
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
-
截图中第41行的
return "redirect:" + url;
表示访问getWxCode方法时会让浏览器地址栏重定向到url,这个url就是我们在"3.1看帮助文档"说的生成二维码的网址https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
3.3测试
1.重启服务,在地址栏输入http://localhost:8006/api/ucenter/wx/login,可以显示的字符串而不是生成的二维码
2.这是因为我们给控制器WxApiController添加了注解@RestController,我们在
"demo03-后台讲师管理模块"的"3.1准备工作"的第2步说过,如果加了这个注解,方法返回字符串时就会直接将字符串写到浏览器中。我们不想让字符串直接写到浏览器上,而是想让浏览器访问生成二维码的网址https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
,所以我们将控制器WxApiController上的@RestController注解改为@Controller注解
3.重启服务,再次访问http://localhost:8006/api/ucenter/wx/login,此时就可以看到成功生成了二维码
3.4扫码成功后跳转规则
1.在微信上确认登录后会显示页面跳转错误
2.我们在"2.3需要域名地址"说过,成功扫描登录后,会去访问我们在"application.properties"中配置的域名地址http://localhost:8160/api/ucenter/wx/callback
看上张截图中地址栏是http://localhost:8160/api/ucenter/wx/callback?code=081Wp1Ga1zvHTD0BUtFa1OGvbg3Wp1Gc&state=atguigu
。可以知道老师给我们配置的服务器内部实际上做了这个操作:携带code、state参数请求我们本地电脑的8160端口的/api/ucenter/wx/callback
3.老师的服务器中这样做是考虑到我们大家并没有余力再构建一个服务器,所以就在他自己的服务器中进行了配置,使得我们访问老师的服务器时实际上最后又访问到了我们本地电脑的8160端口,这样的话我们只需要在项目中8160端口下编写代码就可以了
4.实际开发中我们就没必要和老师一样了:在自己的服务器上配置,然后让访问服务器时最后实际上访问的还是本地的端口。
实际开发中我们是直接将项目部署到服务器上,用户访问服务器时就可以直接访问到服务
5.因为成功扫描登录后地址栏中的地址是http://localhost:8160/api/ucenter/wx/callback?code=081Wp1Ga1zvHTD0BUtFa1OGvbg3Wp1Gc&state=atguigu
,所以我们需要做如下修改:
- 把本地服务端口号改为8160
- 回调接口地址写成和域名跳转地址(/api/ucenter/wx/callback)一样的
①将service_ucenter模块的端口号改为8160
那就顺便需要在nginx中重新进行配置并重启nginx
②我们服务中回调接口地址和域名跳转地址本身就是一样的,所以无需修改
只需要在在控制器WxApiController中编写一个方法实现路径跳转,这个方法的路径必须是callback(因为老师服务器中域名跳转地址是/api/ucenter/wx/callback)
@GetMapping("callback")
public String callback(String code, String state) {
System.out.println("code: " + code);
System.out.println("state: " + state);
return "redirect:http://localhost:3000";
}
6.重启服务,访问http://localhost:8160/api/ucenter/wx/login,成功扫码登录后可以看到浏览器页面跳转到3000端口,并且控制台有输出。至此,就说明我们已配置成功
4.获取扫描人信息
4.1分析
1.获取扫描人信息的业务逻辑人家微信官方已经给我们规定好了,我们必须按照人家的要求来:
第一步:在"3.4扫码成功后跳转规则"的第2步我们说过了,成功扫码登录后,老师的服务器返回的地址中有两个参数:code和state
- 参数state的值就是我们在"3.2控制层"的截图中的第38行的atguigu。这个state暂时用不上,不用管它
- 参数code的值是每次扫码后微信给我们提供的随机唯一值,类似于手机验证码
第二步:拿着第一步获取的code值,请求微信提供的一个固定地址https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
,请求后会获取到两个值:access_token(访问微信操作的凭证)、openid(每个用户微信的唯一标识)
第三步:拿着第二步获取的两个值(access_token、openid)请求微信提供的一个固定地址https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
,就可以获取到微信扫描人信息(比如:微信昵称、头像…)
2.用到的技术:
- httpclient
- 刚才说的第二步和第三部请求地址时都不是在用户的浏览器客户端进行请求,而是程序进行请求。所以我们要用到httpclient来请求地址
- 什么是httpclient:我们以前进行请求都是在浏览器地址栏写url然后按回车进行请求,使用httpclient方式后不需要浏览器也能发送请求
- json转换工具
- 我们在"demo14-注册、登录"的"4.4控制层"中使用的json转换工具是fastjson,这里仍可以使用fastjson,但我们用另一种json转换工具:gson
4.2添加依赖
想要使用刚刚说的httpclient、fastjson需要引入三个依赖,这三个依赖我们在"demo03-后台讲师管理模块"的"2.2.3创建子模块"的第4步就已经添加到service模块中了,无需重复添加
4.3创建httpclient工具类
1.这个工具类老师已经给我们写好了,我放在了资料中
2.我们直接将这个工具类复制到service_ucenter模块的utils包下
4.4控制层
1.先将控制器WxApiController的callback方法中下图用红框圈起来的部分删掉
2.在callback方法中添加如下代码
try {
//1.拿着code(这是一个临时票据,类似于验证码)请求微信给的固定地址
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//1.1给地址拼接三个参数:id、秘钥、code值
String accessTokenUrl = String.format(
baseAccessTokenUrl,
ConstantWxUtils.WX_OPEN_APP_ID,
ConstantWxUtils.WX_OPEN_APP_SECRET,
code);
//1.2使用httpclient请求拼接好的地址,得到openid和access_token
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
//输出accessTokenInfo看一下这个字符串长什么样,以便分析后面的业务逻辑
System.out.println(accessTokenInfo);
//返回到首页面
return "redirect:http://localhost:3000";
} catch (Exception e) {
throw new GuliException(20001, "登录失败");
}
3.重启服务,成功扫描登录后查看控制台的输出
可以看到微信给我们返回的是一个json字符串,并且字符串中有我们想要的openid和access_token,所以我们需要先将这个json字符串转为java的map集合对象,然后得到openid、access_token
4.在callback方法中添加如下代码继续完成业务逻辑
//从json字符串中获取openid和access_token
//使用json工具把accessTokenInfo字符串转换为map集合,根据map中的key获取对应的值
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String)mapAccessToken.get("access_token");
String openid = (String)mapAccessToken.get("openid");
//2.拿着access_token和openid请求微信给的固定地址
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//2.1给地址拼接两个参数:access_token、openid
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
//2.2使用httpclient请求拼接好的地址
String userInfo = HttpClientUtils.get(userInfoUrl);
//输出userInfo看一下这个字符串长什么样,以便分析后面的业务逻辑
System.out.println(userInfo);
5.重启服务,成功扫描登录后查看控制台的输出
- 可以看到微信给我们返回的也是一个json字符串,我们想从这个字符串中得到用户的昵称(nickname)、头像(headimgurl),所以我们需要先将这个json字符串转为java的map集合对象,然后得到我们想要的数据
- 这里的头像地址是
https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/tgLicZzj73baAVqvp0u1FC0zexcvWp2HMKB1aQN6OGIIXQvSgdiczQfw49BFe6SGWRibnGApibdicxyaXDwmQ2U26uw\/132
,可以看到人家用反斜杠(\)给我们做了转义,所以我们直接访问这个地址是访问不到的,我们在后续的业务逻辑中去掉这个反斜杠- 但是后来老师并没有再提过这个事情,并没有去掉这些反斜杠,而且在后面的"6.测试"中也一切正常
6.接下来的业务逻辑中会操作数据库,所以先在控制器注入
@Autowired
private UcenterMemberService memberService;
7.在callback方法中添加如下代码继续完成业务逻辑
//3.获取扫码人信息
//3.1将json字符串转换为map集合
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = (String) userInfoMap.get("nickname"); //用户昵称
String headimgurl = (String) userInfoMap.get("headimgurl"); //用户头像
//4.微信扫码登录时注册的过程不再由用户完成,而是由我们后端直接实现
//所以我们此时需要将用户的openid、昵称、头像加到数据库中
//根据opendi判断数据库中是否已有该用户
UcenterMember member = memberService.getOpenIdMember(openid);
if (member == null) { //数据表中没有这个openid,进行添加
member = new UcenterMember();
member.setOpenid(openid);
member.setNickname(nickname);
member.setAvatar(headimgurl);
memberService.save(member);
}
4.5业务层接口
在业务层接口UcenterMemberService中定义抽象方法
//根据openid判断数据表中是否已有这个用户
UcenterMember getOpenIdMember(String openid);
4.6业务层实现类
在业务层实现类UcenterMemberServiceImpl中实现上一步定义的抽象方法
//根据openid判断数据表中是否已有这个用户
@Override
public UcenterMember getOpenIdMember(String openid) {
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("openid", openid);
UcenterMember member = baseMapper.selectOne(wrapper);
return member;
}
4.7控制层功能完善
先看一下callback方法中的部分代码
我们知道截图中红框圈起来的代码目的是从微信中得到用户的信息,但是如果数据库中此时有这个用户的数据,那就没必要再执行这些代码了,所以我们将这些代码剪切粘贴到if中
5.首页显示信息
5.1分析
1.我们在"demo14-注册、登录"的"7.17将用户信息在页面显示"的第2步的截图的第161行是从cookie中获取用户信息:var userStr = cookie.get('guli_ucenter')
。2.我们这里也可以将微信用户的信息存到cookie中然后再从cookie中获取信息,但这样做有一个问题:
cookie无法实现跨域访问,假如我们这个项目有一个服务是guli.com,还有一个服务是atguigu.com,这是两个不同域名的服务,那么在guili.com中存的cookie,在atguigu.com中是得不到这个cookie的
3.最终解决方案:使用jwt生成token字符串,把token字符串通过路径传递到首页面
5.2控制层整合jwt令牌
1.删除控制器WxApiController的callback方法中下图红框圈起来的代码
2.在控制器WxApiController的callback方法中添加代码
//使用jwt根据member对象(对象中有用户信息)生成token字符串
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
//返回到首页面(通过路径传递token字符串)
return "redirect:http://localhost:3000?token=" + jwtToken;
5.3获取首页路径中的token值
1.我们前面有很多从路径中取值的例子(比如"demo05-讲师管理前端"的"10.3.3调用方法"根据id查询数据""中的this.$route.params.id
),但是以前的例子的路径都是形如http://localhost:3000/edit/123
(123是id),而我们这里要取的token在路径中的形式是http://localhost:3000/?token=xxx
,所以不能用以前的方式来取得token值,正确的做法是使用this.$route.query.token
来取得token值
2.我们在控制器WxApiController的callback方法最后是让回到了首页面
3.进入首页面时是要先进入到布局页面default.vue页面,然后才在布局页面引入首页面login,所以我们在default.vue页面编写代码
//获取路径中的token值
this.token = this.$route.query.token
if(this.token) { //路径中有token值
this.wxLogin()
}
截图中第157行为什么要判断路径中是否有token:首页面路径中如果有token,说明用户是微信扫码登录,我们接着编写后面的逻辑;如果没有token,说明用户是账号密码登录,这种情况下获取用户信息的逻辑我们在"demo14-注册、登录"的"7.16调用后端接口获取用户信息(登录功能)"已经实现了
5.4将token值存入cookie
在"demo14-注册、登录"的"7.15创建前端拦截器(登录功能)"我们创建了前端拦截器,这个拦截器会判断cookie中是否有token,如果有就把cookie里面值取出来放到header里面。所以我们需要将token值存入cookie以便被前端拦截器拦截
//微信登录时显示用户信息
wxLogin() {
//把token值放到cookie里面
cookie.set('guli_token',this.token,{domain: 'localhost'})
},
5.5根据token获取用户信息
1.调用调用api中的方法来根据token获取用户信息,这个方法是在login.js中定义的,所以我们要先在default.vue页面中引入这个js文件
import loginApi from '@/api/login'
2.在wxLogin方法中添加代码
//调用api中的方法来根据token值获取用户信息
loginApi.getLoginUserInfo()
.then(response => {
this.loginInfo = response.data.data.userInfo
})
5.6将获取到的用户信息存入cookie
在wxLogin方法中添加代码
//将用户信息存入cookie
//将json对象转为json字符串,这样才能存到cookie中
var jsonStr = JSON.stringify(this.loginInfo)
cookie.set('guli_ucenter',jsonStr,{domain: 'localhost'})
6.测试
看看后端重启没,看看nginx配置、保存、重启没,看看前端保存没,看看redis启动没
检查完后先清空浏览器的cookie数据然后在地址栏输入http://localhost:8160/api/ucenter/wx/login进行测试,成功扫码登录后可以看到显示了个人数据,并且数据库中也成功插入了这条数据