微信开放平台(针对开发者和公司):https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
OAuth (Open Authorization) 协议就是为用户资源的授权提供了一个安全、开放、简易的标准。
OAuth 在第三方应用与服务提供商之间设置了一个授权层,第三方应用通过授权层获取令牌,再通过令牌获取信息。
令牌与密码的作用都可以进入系统,但是有三点差异:
1、令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
2、令牌可以被数据所有者撤销,会立即失效。
3、令牌有权限范围,比如不能获取用户密码信息。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。
OAuth 的四种授权模式:
1、授权码模式(功能最完整、流程最严密的授权模式)
2、密码模式
3、简化模式
4、客户端模式
应用 ID,唯一标识(身份证号)
应用的密钥
授权的临时凭证(例如:临时身份证)
接口调用凭证(例如:令牌)
用户 -> 使用微信扫码登录第三方应用 -> 微信登录的服务地址回调函数 -> 发出申请 —> 微信账号
微信账号 -> 发送确认登录
用户 -> 点击确认
微信账号 -> 重定向到第三方应用提供的函数,并携带 Authorization Code
用户 -> 使用 Authorization Code + App ID + App Secret 到微信账号换取 Access Token
微信账号 -> 返回 Access Token
用户 -> 通过 Access Token 获取用户的信息
微信账号 -> 返回对应的用户信息
微信官方提供的生成二维码的 js:
npm install vue-wxlogin
如果不是 Vue 的项目,可以直接引用官方提供的 js 文件,来生成二维码。
src\components\Header\Header.vue
<div id="loginForm">
<!-- 登录表单 -->
<el-form/>
<!-- 登录按钮 -->
<el-button/>
<!-- 微信登录图标 -->
<img/>
</div>
<!-- 二维码 -->
<wxlogin id="wxLoginForm" style="display:none"
:appid="appid" :scope="scope" :redirect_uri="redirect_uri">
</wxlogin>
<script>
import wxlogin from 'vue-wxlogin'; // 引入
export default {
name: "Header",
components: {
wxlogin // 声明
},
data() {
return {
appid:"wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope:"snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
redirect_uri:"http://www.pinzhi365.com/wxlogin", //重定向地址,(回调地址)
};
},
methods: {
goToLoginWX() {
document.getElementById("loginForm").style.display = "none";
document.getElementById("wxLoginForm").style.display = "block";
}
}
}
</script>
<!-- 需要使用 HttpServletRequest 获得参数 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
<!-- 需要使用 HttpClient 发出请求 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
回调默认指定的是 80 端口
127.0.0.1 www.pinzhi.com
commons.HttpClientUtil
package commons;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.net.URI;
import java.util.Map;
/**
* HttpClient 的封装工具类
*
* @author Renda Zhang
* @since 2020-10-27 0:28
*/
public class HttpClientUtil {
public static String doGet(String url) {
return doGet(url, null);
}
/**
* Get 请求,支持 request 请求方式,不支持 restful 方式
*
* @param url 请求地址
* @param param 参数
* @return 响应的字符串
*/
public static String doGet(String url, Map<String, String> param) {
// 创建 httpclient 对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建 url
URIBuilder builder = new URIBuilder(url);
if (param != null) {
// 在 url 后面拼接请求参数
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建 http get 请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpClient.execute(httpGet);
// 从响应对象中获取状态码(成功或失败的状态)
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应的状态 = " + statusCode);
// 200 表示响应成功
if (statusCode == ) {
// 响应的内容字符串
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return resultString;
}
}
entity.Token
package entity;
/**
* 令牌实体类
*
* @author Renda Zhang
* @since 2020-10-27 0:33
*/
public class Token {
// 接口调用凭证
private String access_token;
// access_token 接口调用凭证超时时间,单位(秒)
private String expires_in;
// 用户刷新 access_token
private String refresh_token;
// 授权用户唯一标识
private String openid;
// 用户授权的作用域,使用逗号分隔
private String scope;
// 当且仅当该网站应用已获得该用户的 user info 授权时,才会出现该字段。
private String unionid;
public Token() {
}
public Token(String access_token, String expires_in, String refresh_token, String openid, String scope, String unionid) {
this.access_token = access_token;
this.expires_in = expires_in;
this.refresh_token = refresh_token;
this.openid = openid;
this.scope = scope;
this.unionid = unionid;
}
// getter and setter ...
}
entity.WxUser
package entity;
/**
* 微信的用户信息
*
* @author Renda Zhang
* @since 2020-10-27 0:33
*/
public class WxUser {
// 普通用户的标识,对当前开发者帐号唯一
private String openid;
// 普通用户昵称
private String nickname;
// 普通用户性别,1 为男性,2 为女性
private String sex;
// 普通用户个人资料填写的省份
private String province;
// 普通用户个人资料填写的城市
private String city;
// 国家,如中国为 CN
private String country;
// 用户头像,最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640*640 正方形头像),用户没有头像时该项为空
private String headimgurl;
// 用户特权信息,json 数组,如微信沃卡用户为 chinaunicom
private String privilege;
// 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的。
private String unionid;
public WxUser() {
}
public WxUser(String openid, String nickname, String sex, String province, String city, String country, String headimgurl, String privilege, String unionid) {
this.openid = openid;
this.nickname = nickname;
this.sex = sex;
this.province = province;
this.city = city;
this.country = country;
this.headimgurl = headimgurl;
this.privilege = privilege;
this.unionid = unionid;
}
// getter and setter ...
}
com.renda.wx.WxLoginController
package com.renda.wx;
import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.fastjson.JSON;
import com.renda.entity.User;
import com.renda.entity.UserDTO;
import com.renda.user.UserService;
import commons.HttpClientUtil;
import entity.Token;
import entity.WxUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 微信支付相关控制
*
* @author Renda Zhang
* @since 2020-10-27 0:41
*/
@RestController
@RequestMapping("user")
public class WxLoginController {
@Reference // dubbo 远程消费
private UserService userService;
// 是否用微信登录成功,dto 为 null,则尚未登录
private UserDTO dto = null;
@GetMapping("wxlogin")
public String wxlogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 微信官方发给我们一个临时凭证
String code = request.getParameter("code");
System.out.println("【临时凭证】code = " + code);
// 2. 通过 code,去微信官方申请一个正式的 token(令牌)
String getTokenByCode_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code=" + code + "&grant_type=authorization_code";
String tokenString = HttpClientUtil.doGet(getTokenByCode_url);
System.out.println("tokenString = " + tokenString);
// 将 json 格式的 token 字符串转换成实体对象,方便存和取
Token token = JSON.parseObject(tokenString, Token.class);
// 3. 通过 token,去微信官方获取用户的信息
String getUserByToken_url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + token.getAccess_token() + "&openid=" + token.getOpenid();
String userinfoString = HttpClientUtil.doGet(getUserByToken_url);
System.out.println("userinfoString = " + userinfoString);
// 将 json格式的user字符串转换成实体对象,方便存和取
WxUser wxUser = JSON.parseObject(userinfoString, WxUser.class);
System.out.println("微信昵称 = " + wxUser.getNickname());
System.out.println("微信头像 = " + wxUser.getHeadimgurl());
// 业务流程:需要手机号 wxUser.getUnionid() 和密码 wxUser.getUnionid(),头像和昵称
User user = null;
dto = new UserDTO();
// 检测手机号是否注册
Integer i = userService.checkPhone(wxUser.getUnionid());
if(i == ){
// 未注册,自动注册并登录
userService.register(wxUser.getUnionid(), wxUser.getUnionid(),wxUser.getNickname(),wxUser.getHeadimgurl());
dto.setMessage("手机号尚未注册,系统已帮您自动注册,请牢记密码!");
user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
}else{
user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
if(user == null){
// 300 表示失败
dto.setState();
dto.setMessage("帐号密码不匹配,登录失败!");
}else{
// 200 表示成功
dto.setState();
dto.setMessage("登录成功!");
}
}
dto.setContent(user);
response.sendRedirect("http://localhost:8080");
return null;
}
@GetMapping("checkWxStatus")
public UserDTO checkWxStatus(){
return this.dto;
}
@GetMapping("logout")
public Object logout(){
this.dto = null;
return null;
}
}
src\components\Header\Header.vue
<script>
created(){
// 当刷新页面,组件创建成功之后,立刻检测本地储存中是否存在用户对象
this.userDTO = JSON.parse( localStorage.getItem("user") );
if(this.userDTO != null){
// 已登录
this.isLogin = true;
}else{
// 去检测微信是否登录过
this.axios.get("http://localhost:80/user/checkWxStatus")
.then((result) => {
this.userDTO = result.data;
this.phone = this.userDTO.content.phone;
this.password = this.userDTO.content.password;
// 走普通登录
this.login();
}).catch((error) => {
this.$message.error("登录失败!");
});
}
}
// 登出
logout(){
// 将登录成功的对象信息保存到本地储存中
localStorage.setItem("user", null);
// 更新登录状态
this.isLogin = false;
alert('谢谢使用,再见!');
// 去检测微信是否登录过
this.axios.get("http://localhost:80/user/logout")
.then( (result)=>{})
.catch( (error)=>{
this.$message.error("登录失败!");
});
}
</script>
谷歌浏览器调试的时候,iframe
标签跨域问题导致无法跳转的 bug。
如果 iframe
未添加 sandbox
属性,或者 sandbox
属性不赋值,就代表采用默认的安全策略。
即 iframe
的页面将会被当做一个独立的源,并且不能提交表单,不能执行 JavaScript 脚本,也不能让包含 iframe
的父页面导航到其他地方,所有的插件,如 Flash 等也全部不能起作用。
简单来说 iframe
就只剩下一个展示数据的功能,正如他的名字一样,所有的内容都被放进了一个“单独的沙盒”。
Sandbox 包含的属性及作用:
allow-scripts
- 允许运行执行脚本allow-top-navigation
- 允许 iframe 能够主导 window.top
进行页面跳转allow-same-origin
- 允许同域请求,比如 Ajax、storageallow-forms
- 允许进行提交表单allow-popups
- 允许 iframe 中弹出新窗口,比如 window.open
、target=”_blank”
allow-pointer-lock
- 在 iframe 中可以锁定鼠标,主要和鼠标锁定有关加上 sandbox=“allow-scripts allow-top-navigation allow-same-origin”
属性,即可解决。
官方 js:http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
因为无法修改微信服务器上的 js 文件,所以将 js 代码放在本地并进行修改:
src\components\Header\Header.vue
created() {
// 当刷新页面,组件创建成功之后,立刻检测本地储存中是否存在用户对象
this.userDTO = JSON.parse(localStorage.getItem("user"));
if (this.userDTO != null) {
// 已登录
this.isLogin = true;
} else {
// 去检测微信是否登录过
this.axios
.get("http://localhost:80/user/checkWxStatus")
.then((result) => {
this.userDTO = result.data;
this.phone = this.userDTO.content.phone;
this.password = this.userDTO.content.password;
// 走普通登录
this.login();
})
.catch((error) => {
this.$message.error("登录失败!");
});
}
!(function (a, b, c) {
function d(a) {
var c = "default";
a.self_redirect === !
? (c = "true")
: a.self_redirect === ! && (c = "false");
var d = b.createElement("iframe"),
e =
"https://open.weixin.qq.com/connect/qrconnect?appid=" +
a.appid +
"&scope=" +
a.scope +
"&redirect_uri=" +
a.redirect_uri +
"&state=" +
a.state +
"&login_type=jssdk&self_redirect=" +
c +
"&styletype=" +
(a.styletype || "") +
"&sizetype=" +
(a.sizetype || "") +
"&bgcolor=" +
(a.bgcolor || "") +
"&rst=" +
(a.rst || "");
(e += a.style ? "&style=" + a.style : ""),
(e += a.href ? "&href=" + a.href : ""),
(d.src = e),
(d.frameBorder = "0"),
(d.allowTransparency = "true"),
// 允许多种请求
(d.sandbox = "allow-scripts allow-top-navigation allow-same-origin"),
(d.scrolling = "no"),
(d.width = "300px"),
(d.height = "400px");
var f = b.getElementById(a.id);
(f.innerHTML = ""), f.appendChild(d);
}
a.WxLogin = d;
})(window, document);
},
src\components\Course.vue
<div id="wxLoginForm"></div>
methods: {
// 微信登录
goToLoginWX() {
// 普通的登录表单隐藏
document.getElementById("loginForm").style.display = "none";
// 显示二维码的容器
document.getElementById("wxLoginForm").style.display = "block";
// 去生成二维码
this.$nextTick(function(){
// 直接调用会报错:TypeError: Cannot read property 'appendChild' of null
this.createCode();
});
},
// 生成二维码
createCode(){
var obj = new WxLogin({
// 显示二维码的容器
id:"wxLoginForm",
// 应用唯一标识,在微信开放平台提交应用审核通过后获得
appid: "wxd99431bbff8305a0",
// 应用授权作用域,网页应用目前仅填写 snsapi_login 即可
scope: "snsapi_login",
// 重定向地址,回调地址
redirect_uri: "http://www.pinzhi365.com/wxlogin",
href: "data:text/css;base64,加密后的样式"
});
},
}
用站长工具对样式代码进行 base64 加密:http://tool.chinaz.com/Tools/Base64.aspx
.impowerBox .qrcode {width: 200px;}
.impowerBox .title {display: none;}
.impowerBox .info {width: 200px;}
.status_icon {display: none}cs
.impowerBox .status {text-align: center;}