显示登陆二维码
微信OAuth2.0授权登录目前支持authorization_code
模式,适用于拥有server端的应用授权,详情查看接口文档
在微信开放平台注册身份并添加网站应用,添加应用时需要指定网站的外网域名
作为用户授权后微信的回调域名,审核通过后查看生成的appID和AppSecret
内网穿透
如果我们的开发环境在局域网,而微信回调时需要指向一个公网域名,此时需要使用内网穿透技术让微信回调请求至我们的开发计算机上
内网穿透简单来说就是使用内网穿透服务器
作为中介让外网可以间接访问内网获取数据
在本地电脑上安装内网穿透服务器的客户端如natapp
,配置外网域名
及其要访问的内网地址及端口
,开启隧道之后会分配一个专属域名/端口
跳转二维码
第一步:跳转到微信域下生成的二维码页面
// 天机学堂
https://open.weixin.qq.com/connect/qrconnect?
appid=wx17655f8047b85150
// 重定向的网址
&redirect_uri=http://tjxt-user-t.itheima.net/api/auth/wxLogin
&response_type=code
&scope=snsapi_login
#wechat_redirect
// 我的谷粒
https://open.weixin.qq.com/connect/qrconnect?
appid=wxed9954c01bb89b47
// 重定向的网址
&redirect_uri=http://localhost:8160/auth/wxLogin
&response_type=code
&scope=snsapi_login
#wechat_redirect
第二步:用户允许授权后,将会重定向到指定的网址
上并且带上code和state
参数,若用户禁止授权则不会发生重定向
redirect_uri?code=CODE&state=STATE
内嵌二维码
第一步: 在前端工程xc-ui-pc-static-portal
目录下的wxsign.html
页面中先引入wxLogin.js(支持https)
文件http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
- 用户使用微信扫码授权后通过JS将code返回给网站,用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率
!function(e,t){
e.WxLogin=function(e){
var r="default";
!0===e.self_redirect?r="true":!1===e.self_redirect&&(r="false");
var n=t.createElement("iframe"),
i="https://open.weixin.qq.com/connect/qrconnect? appid="+e.appid+"&scope="+e.scope+"&redirect_uri="+e.redirect_uri+"&state="+e.state+"&login_type=jssdk&self_redirect="+r+"&styletype="+(e.styletype||"")+"&sizetype="+(e.sizetype||"")+"&bgcolor="+(e.bgcolor||"")+"&rst="+(e.rst||"");
i+=e.style?"&style="+e.style:"",
i+=e.href?"&href="+e.href:"",
i+="en"===e.lang?"&lang=en":"",
n.src=i,n.frameBorder="0",
n.allowTransparency="true",
n.scrolling="no",
n.width="300px",
n.height="400px";
var s=t.getElementById(e.id);
s.innerHTML="",
s.appendChild(n)
}
}(window,document);
第二步: 在需要使用微信登录的地方使用wxObj
对象,此时页面上就会出现二维码了
// 我的谷粒
var wxObj = new WxLogin({
self_redirect:true,
id:"login_container",
appid: "wxed9954c01bb89b47",
scope: "snsapi_login",
redirect_uri: "http://localhost:8160/auth/wxLogin",// 扫描二维码授权后重定向到认证服务下的auth/wxLogin接口
state: token,
style: "",
href: ""
});
// 天机学堂
function generateWxQrcode(token) {
var wxObj = new WxLogin({
self_redirect:true,
id:"login_container",
appid: "wx17655f8047b85150",
scope: "snsapi_login",
redirect_uri: "http://tjxt-user-t.itheima.net/api/auth/wxLogin",// 扫描二维码授权后重定向到认证服务下的auth/wxLogin接口
state: token,
style: "",
href: ""
});
微信扫码认证
微信接口文档
访问接口获取用户信息,snsapi_base
属于基础接口,若应用已拥有其它scope权限则默认拥有snsapi_base的权限,详情查看接口文档
- snsapi_base可以让移动端网页授权时
绕过跳转授权登录页请求用户授权的动作
,直接跳转第三方网页带上授权临时票据code - snsapi_base会使得用户已授权作用域(scope)仅为snsapi_base,从而导致无法获取到需要用户授权才允许获得的数据和基础功能
接口作用域 | 接口 | 说明 |
---|---|---|
snsapi_base | https://api.weixin.qq.com/sns/oauth2/access_token | 通过 code 换取access token、refresh token和已授权scope |
snsapi_base | https://api.weixin.qq.com/sns/oauth2/refresh_token | 刷新或续期access_token使用 |
snsapi_base | https://api.weixin.qq.com/sns/auth | 检access_token有效性 |
snsapi_userinfo | https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID | 获取用户个人信息 |
接口请求/响应参数说明
参数 | 是否必须 | 说明 |
---|---|---|
self_redirect | 否 | true :手机点击确认登录后可以在 iframe 内跳转到 redirect_urifalse(默认) :手机点击确认登录后可以在 top window 跳转到 redirect_uri |
id | 是 | 第三方页面显示二维码的容器id |
appid | 是 | 应用唯一标识,在微信开放平台提交应用审核通过后获得 |
scope(应用授权作用域) | 是 | 如果拥有多个作用域使用逗号分隔,网页应用目前仅填写snsapi_login 即可 |
redirect_uri | 是 | 重定向地址,需要进行UrlEncode 用户授权后重定向到 redirect_uri 的网址上并且带上code和state参数 ,若用户禁止授权则不会发生重定向 |
response_type | code | |
state | 否 | 用于保持请求和回调的状态,授权请求后原样带回给第三方 该参数可用于防止 csrf 攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加 session 进行校验 |
style | 否 | 提供black(默认)、white 可选 |
href | 否 | 自定义样式链接,第三方可根据实际需求覆盖默认样式 |
code(授权码) | 是 | 用户授权后微信服务端返回给第三方应用的参数 |
grant_type | 是 | 微信认证 :authorrization_code,密码模式 : password |
access_token | 第三方应用调用微信接口的凭证 | |
expires_in | 接口调用凭证的超时时间(秒) | |
refresh_token | 用户刷新access_token | |
scope(用户授权作用域) | 用户授权作用域 |
重定向接收授权码
用户扫描二维码授权后会重定向到认证服务下的localhost:8160/auth/wxLogin
接口并且带上code和state
参数,这个重定向的地址需要在微信开放平台申请
第一步:在认证服务工程
中定义接口接收微信下发的授权码,我们需要根据授权码远程调用微信的认证服务申请令牌,最后用令牌调用微信提供的获取用户信息
接口
@Slf4j
@Controller// 因为要实现重定向这里只能使用@Controller
public class WxLoginController {
@Autowired
WxAuthServiceImpl wxAuthService;
@RequestMapping("/wxLogin")
public String wxLogin(String code, String state) throws IOException {
log.debug("微信扫码回调,code:{},state:{}",code,state);
// 根据授权码远程调用微信的认证服务申请令牌,拿到令牌后调用微信提供的获取用户信息的接口,将获取到的用户信息写入本项目的用户信息数据库并返回
XcUser xcUser = wxAuthService.wxAuth(code);
if(xcUser==null){
return "redirect:http://www.51xuecheng.cn/error.html";
}
String username = xcUser.getUsername();
// 重定向到登陆页面自动登录,此时页面会访问的们的认证服务,认证逻辑是微信认证中实现的execute方法(判断数据库中是否有获取到的用户信息)
return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
}
}
第二步:在WxAuthServiceImpl
重写wxAuth
方法实现根据授权码远程调用微信认证服务申请令牌
,拿到令牌后调用微信服务接口查询用户信息
并保存
public interface WxAuthService {
XcUser wxAuth(String code);
}
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService, WxAuthService {
// 注入代理对象,非事务方法调用事务方法要使用代理对象调用防止事务失效
@Autowired
WxAuthServiceImpl wxAuthService;
@Override
public XcUser wxAuth(String code) {
// 携带授权码远程调用微信服务提供的接口获取微信服务颁发的令牌
Map<String, String> access_token_map = getAccess_token(code);
if(access_token_map==null){
return null;
}
String accessToken = access_token_map.get("access_token");
// 携带令牌和授权用户的唯一标识远程调用微信服务提供的接口查询用户信息
String openid = access_token_map.get("openid");
Map<String, String> user_info_map = getUserInfo(accessToken, openid);
if(user_info_map == null){
return null;
}
// 保存用户信息到本项目数据库(先查询如果有直接返回),这里需要使用代理对象调用
XcUser xcUser = wxAuthService.addWxUser(user_info_map);
return xcUser;
}
}
申请令牌
第一步: 在AuthApplication
里注册RestTemplate
到容器中,由于微信的认证服务属于对于第三方系统,所以需要使用RestTemplate发起请求申请令牌
@Bean
RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
return restTemplate;
}
第二步:在nacos中的auth-service-dev.yaml
配置文件中配置认证服务的端口要与微信中配置的回调域名端口一致,配置申请的Appid和Appsecret
# 我的谷粒,http://localhost:8160/auth/wxLogin
server:
servlet:
context-path: /auth
port: 8160
weixin:
appid: wxed9954c01bb89b47
secret: a7482517235173ddb4083788de60b90e
# 天机学堂,http://tjxt-user-t.itheima.net/api/auth/wxLogin
server:
servlet:
context-path: /auth
port: 63070
weixin:
appid:wx17655f8047b85150
secret: 68918d65287802a19b1905cbda7eaa93
第三步:在WxAuthServiceImpl
类中定义申请令牌的私有方法,携带授权码,Appid,密钥
远程调用微信服务提供的的sns/oauth2/access_token
接口获取令牌
@Autowired
RestTemplate restTemplate;
@Value("${weixin.appid}")
String appid;
@Value("${weixin.secret}")
String secret;
private Map<String, String> getAccess_token(String code) {
// 1. 请求路径模板,参数用%s占位符
String url_template = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
// 2. 填充占位符:appid,secret,code
String url = String.format(url_template, appid, secret, code);
// 3. POST方式远程调用URL,指定请求方式,请求内容,响应结果
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, null, String.class);
// 4. 获取相应结果,响应结果为json格式的字符串
String result = exchange.getBody();
// 5. 将JSON数组转为map
Map<String, String> map = JSON.parseObject(result, Map.class);
return map;
}
第四步: 查看响应的令牌信息是个JSON数组
// 申请访问令牌的成功响应示例
{
"access_token":"ACCESS_TOKEN",// 第三方应用调用微信接口的凭证
"expires_in":7200,// 接口调用凭证的超时时间(秒)
"refresh_token":"REFRESH_TOKEN",// 用户刷新access_token
"openid":"OPENID",// 授权用户在微信服务上的唯一标识
"scope":"SCOPE",// 用户授权作用域
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"// 如果我们在注册的微信平台账号上添加了多个网站,unionid是用户在我们这个注册账号上的唯一Id
}
// 错误的响应实例
{"errcode":40029,"errmsg":"invalid code"}
获取用户信息
第一步: 携带令牌调用查询用户信息接口/sns/userinfo
,需要保证令牌有效且未超时并且微信用户已授权给第三方应用帐号相应接口作用域
即可以调用哪些接口
private Map<String, String> getUserInfo(String access_token, String openid) {
// 1. 请求路径模板,参数用%s占位符
String url_template = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s";
// 2. 填充占位符,access_token和openid
String url = String.format(url_template, access_token, openid);
// 3. GET方式远程调用URL
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
// 4. 获取响应结果JSON格式的字符串,默认以ISO_8859_1格式编码
String result = exchange.getBody();
// 4.1 转码解决响应结果包含中文时的乱码问题
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// 5. 将JSON数组转为map
Map<String, String> map = JSON.parseObject(result, Map.class);
return map;
}
第二步: 查看微信服务端响应的用户信息JSON格式的字符串
{
"openid":"OPENID",// 普通授权用户的标识,对当前开发者帐号唯一
"nickname":"NICKNAME",// 普通用户昵称,如果昵称包含中文接收时需要转码
"sex":1,// 普通用户性别,1为男性,2为女性
"province":"PROVINCE",// 普通用户个人资料填写的省份
"city":"CITY",// 普通用户个人资料填写的城市
"country":"COUNTRY",
// 用户头像,可以在浏览器直接访问,最后一个数值代表正方形头像大小,(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
"headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
// 用户特权信息,如微信沃卡用户为chinaunicom
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
// 用户统一标识,当前仅当网站应用已获得该用户的userinfo授权时,才会出现该字段
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
保存用户信息
判断微信服务中查询到的用户在本项目用户数据库中是否存在,如果不存在则将其信息保存在本项目xc_users
数据库的xc_user
表中并返回
- 保存用户信息时涉及到了多表操作所以需要进行事务控制, 另外
非事务方法调用事务方法
要使用代理对象调用防止事务失效
角色表
用户角色关系表(多对多)
@Autowired
XcUserMapper xcUserMapper;
@Autowired
XcUserRoleMapper xcUserRoleMapper;
@Transactional
public XcUser addWxUser(Map<String, String> user_info_map){
// 1. 获取用户的唯一标识unionid
String unionid = user_info_map.get("unionid");
// 2. 根据unionid判断用户表中是否存在该用户
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid));
// 2.1 存在则直接返回
if (xcUser != null){
return xcUser;
}
// 2.2 不存在添加用户信息到用户表
xcUser = new XcUser();
// 2.3 设置主键
String uuid = UUID.randomUUID().toString();
xcUser.setId(uuid);
// 2.4 设置其他数据库非空约束的属性
xcUser.setUsername(unionid);
xcUser.setPassword(unionid);
xcUser.setWxUnionid(unionid);
xcUser.setNickname(user_info_map.get("nickname"));// 用户昵称
xcUser.setUserpic(user_info_map.get("headimgurl"));// 用户头像
xcUser.setName(user_info_map.get("nickname"));
xcUser.setUtype("101001"); // 用户类型,101001表示学生类型
xcUser.setStatus("1");// 用户状态
xcUser.setCreateTime(LocalDateTime.now());
xcUserMapper.insert(xcUser);
// 3.添加用户信息到用户角色关系表
XcUserRole xcUserRole = new XcUserRole();
xcUserRole.setId(uuid);
xcUserRole.setUserId(uuid);
xcUserRole.setRoleId("17");// 17表示学生角色
xcUserRole.setCreateTime(LocalDateTime.now());
xcUserRoleMapper.insert(xcUserRole);
return xcUser;
}
重定向扫码认证校验
当执行完WxLoginController
中的wxLogin
方法后用户信息一定会被保存到本项目的用户数据库中
重定向本项目的认证服务并携带username=unionid&authType=wx
进行扫码登陆,重写AuthService
的execute
方法实现微信登陆的验证逻辑
- 判断本项目用户数据库中是否有该用户信息,有则登陆成功右上角显示登录用户名
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService, WxAuthService {
/**
* 微信扫码认证,不需要校验密码和验证码,只需要判断用户是否存在
* @param authParamsDto 认证参数
* @return
*/
@Override
public XcUserExt execute(AuthParamsDto authParamsDto) {
// 获取账号即unionid
String username = authParamsDto.getUsername();
// 查询用户数据库判断用户是否存在
XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
if (user == null) {
throw new RuntimeException("账号不存在");
}
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(user, xcUserExt);
return xcUserExt;
}
}