本文前端主要采用 wxlogin 内嵌二维码.(需要先到链接: 微信平台申请,申请后会生成一个id和secret)
前言
目前用户使用微信登录第三方应用或网站,无非两种展现方式
- 第一种, 网页外链跳转方式
- 第二种, 网页内嵌二维码方式
- 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
- 通过code参数加上AppID和AppSecret等,通过API换取access_token;
- 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。 参数说明
| 参数 | 是否必须 | 说明 |
|---|---|---|
| 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"可选,默认为黑色文字描述。详见文档底部FAQ |
| href | 否 | 自定义样式链接,第三方可根据实际需求覆盖默认样式。详见文档底部FAQ |
在这里主要说的是第二种,将微信二维码内嵌到网站页面中,在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。
这里使用vue-wxlogin,方便组件化模块化引入
//安装vue-wxlogin
npm install vue-wxlogin --save-dev
//引入页面
import wxlogin from 'vue-wxlogin';
// 注册组件
components: {
wxlogin,
},
前端代码:在扫码之后网页会跳转,在created获取路由的信息,拿到路由中的code信息,然后请求后台,完成登录。
<template>
<h2>请扫码登录</h2>
<wxlogin
class="login"
:appid= appid
:scope="'snsapi_login'"
:theme="'black'"
:redirect_uri='encodeURIComponent(redirect_url)'
:href="'data:text/css;base64,LmltcG93ZXJCb3ggLnRpdGxlIHsKICBkaXNwbGF5OiBub25lOwp9Ci5pbXBvd2VyQm94IC5xcmNvZGUgewogIHdpZHRoOiAyMjVweDsKICBoZWlnaHQ6IDIyNXB4Owp9'"
:state="`${Math.random()}`"
:delay="5000"
>
</wxlogin>
</template>
<script>
import wxlogin from 'vue-wxlogin';
import {ElMessage} from "element-plus";
export default {
name: "loginView",
data() {
return {
appid: '',
redirect_url:'',
};
},
// 注册组件
components: {
wxlogin,
},
mounted() {
//获取微信appid
this.wxobtain();
},
created() {
if (this.$route.redirectedFrom) {
if (this.$route.redirectedFrom.query.code) {
let openid = this.$route.redirectedFrom.query.code
this.$axios.get('/userInfo/callback/' + openid).then((response) => {
if (response.code === 200) {
// 存储token开始时间
window.localStorage.setItem('tokenStartTime', new Date().getTime())
window.localStorage.setItem('token', response)
this.$router.push("/trackLive");
ElMessage({
message: "登录成功",
type: "success",
});
} else if (response.code === 201) {
ElMessage({
message: "不存在改用户",
type: "error",
});
} else {
this.$message({
message: response.body.msg,
type: 'error'
})
}
})
}
}
},
methods: {
wxobtain(){
this.$axios
.get("/userInfo/wxlogin")
.then((response) => {
//获取appid
const regex = /appid=([^&]+)/;
const match = response.match(regex);
//获取扫码后跳转的地址
const url = new URL(response);
this.redirect_url = decodeURIComponent(url.searchParams.get('redirect_uri'));
if (match) {
this.appid = match[1];
} else {
console.log('Appid not found');
}
});
}
},
};
</script>
<style src="../style/login_style.css" scoped>
</style>
yml配置:微信登录所需信息:这个redirect_url 这个端口在微信开发平台设定好。我这里定义的是前端端口
wx:
open:
app_id: wxed8945sdfwe**
app_secret: a748251723517*****8**
redirect_url: http://127.0.0.1:8080/trackLive
引入
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConstantWxPropertiesUtils implements InitializingBean {
@Value("${wx.open.app_id}")
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;
}
}
后端代码:方法中生成jwt的方法可以注释掉
//一、扫码登录
@GetMapping("wxlogin")
public String getWxCode() {
//微信开放平台baseurl,%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";
String redirectUri = null;
try {
redirectUri = URLEncoder.encode(ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//设置%s里面的值
String url = String.format(baseUrl, ConstantWxPropertiesUtils.WX_OPEN_APP_ID, redirectUri, "hjh520");
//重定向到微信地址里面
return url;
}
//二、验证扫码登录人员信息
@GetMapping("callback/{code}")
public R callback(@PathVariable(value = "code") String code) {
try {
//baseAccessTokenUrl,%s相当于?占位符
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//设置%s里面的值
String url = String.format(baseAccessTokenUrl, ConstantWxPropertiesUtils.WX_OPEN_APP_ID, ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET, code);
//使用httpClient发送请求,得到返回值
String accessTokenInfo = HttpUtils.get(url);
//解析用户信息
JSONObject userInfoJson = JSONObject.parseObject(accessTokenInfo);
//使用gson将字符串转换成对象
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String accessToken = (String) mapAccessToken.get("access_token");
String openid = (String) mapAccessToken.get("openid");
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//拼接两个参数
String userInfoUrl = String.format(
baseUserInfoUrl,
accessToken,
openid);
//发送请求
String userInfo = HttpUtils.get(userInfoUrl);
//获取返回userInfo字符串扫描人信息
HashMap<String, String> userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = userInfoMap.get("nickname");
// String headimgurl = userInfoMap.get("headimgurl");
//判断数据表里面是否存在相同我微信信息,根据openid判断
UserInfo ucenterMember = userInfoService.getMemberByOponId(openid);
if (ucenterMember == null) {
UserInfo user = new UserInfo();
user.setUserName("outsider");
List<UserInfo> userInfoList = userInfoService.queryByUser(user);
user.setOpenid(openid);
user.setPassword("NO-NEED");
user.setWeChatName(nickname);
user.setLevel(0);
//防止外人一直扫码登录,限制保留部分人的信息
if (userInfoList.size() > 10) {
return R.fail();
}
userInfoService.insert(user);
return R.fail();
}
//使用JWT生成token
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID, "mtl");
String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
//最后:返回首页
//也可以返回token前端自己去跳转页面
return R.ok(token);
} catch (Exception e) {
}
return R.fail();
}
http工具类:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
/**
* HttpClient工具类
*
* @author zyf
* @since 2024-01-10
*/
public class HttpUtils {
/**
* 字符编码
*/
private final static String UTF8 = "utf-8";
/**
* 字节流数组大小(1MB)
*/
private final static int BYTE_ARRAY_LENGTH = 1024 * 1024;
/**
* 执行get请求获取响应
*
* @param url 请求地址
* @return 响应内容
*/
public static String get(String url) {
return get(url, null);
}
/**
* 执行get请求获取响应
*
* @param url 请求地址
* @param headers 请求头参数
* @return 响应内容
*/
public static String get(String url, Map<String, String> headers) {
HttpGet get = new HttpGet(url);
return getRespString(get, headers);
}
/**
* 执行post请求获取响应
*
* @param url 请求地址
* @return 响应内容
*/
public static String post(String url) {
return post(url, null, null);
}
/**
* 执行post请求获取响应
*
* @param url 请求地址
* @param params 请求参数
* @return 响应内容
*/
public static String post(String url, Map<String, String> params) {
return post(url, null, params);
}
/**
* 执行post请求获取响应
*
* @param url 请求地址
* @param headers 请求头参数
* @param params 请求参数
* @return 响应内容
*/
public static String post(String url, Map<String, String> headers, Map<String, String> params) {
HttpPost post = new HttpPost(url);
post.setEntity(getHttpEntity(params));
return getRespString(post, headers);
}
/**
* 执行post请求获取响应(请求体为JOSN数据)
*
* @param url 请求地址
* @param json 请求的JSON数据
* @return 响应内容
*/
public static String postJson(String url, String json) {
return postJson(url, null, json);
}
/**
* 执行post请求获取响应(请求体为JOSN数据)
*
* @param url 请求地址
* @param headers 请求头参数
* @param json 请求的JSON数据
* @return 响应内容
*/
public static String postJson(String url, Map<String, String> headers, String json) {
HttpPost post = new HttpPost(url);
post.setHeader("Content-type", "application/json");
post.setEntity(new StringEntity(json, UTF8));
return getRespString(post, headers);
}
/**
* 下载文件
*
* @param url 下载地址
* @param path 保存路径(如:D:/images,不传默认当前工程根目录)
* @param fileName 文件名称(如:hello.jpg)
*/
public static void download(String url, String path, String fileName) {
HttpGet get = new HttpGet(url);
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
String filePath = null;
if (Objects.isNull(path) || path.isEmpty()) {
filePath = fileName;
} else {
if (path.endsWith("/")) {
filePath = path + fileName;
} else {
filePath += path + "/" + fileName;
}
}
File file = new File(filePath);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try (FileOutputStream fos = new FileOutputStream(file); InputStream in = getRespInputStream(get, null)) {
if (Objects.isNull(in)) {
return;
}
byte[] bytes = new byte[BYTE_ARRAY_LENGTH];
int len = 0;
while ((len = in.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取请求体HttpEntity
*
* @param params 请求参数
* @return HttpEntity
*/
private static HttpEntity getHttpEntity(Map<String, String> params) {
List<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
for (Entry<String, String> entry : params.entrySet()) {
pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
HttpEntity entity = null;
try {
entity = new UrlEncodedFormEntity(pairs, UTF8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return entity;
}
/**
* 设置请求头
*
* @param request 请求对象
* @param headers 请求头参数
*/
private static void setHeaders(HttpUriRequest request, Map<String, String> headers) {
if (Objects.nonNull(headers) && !headers.isEmpty()) {
// 请求头不为空,则设置对应请求头
for (Entry<String, String> entry : headers.entrySet()) {
request.setHeader(entry.getKey(), entry.getValue());
}
} else {
// 请求为空时,设置默认请求头
request.setHeader("Connection", "keep-alive");
request.setHeader("Accept-Encoding", "gzip, deflate, br");
request.setHeader("Accept", "*/*");
request.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36");
}
}
/**
* 执行请求,获取响应流
*
* @param request 请求对象
* @return 响应内容
*/
private static InputStream getRespInputStream(HttpUriRequest request, Map<String, String> headers) {
// 设置请求头
setHeaders(request, headers);
// 获取响应对象
HttpResponse response = null;
try {
response = HttpClients.createDefault().execute(request);
} catch (Exception e) {
e.printStackTrace();
return null;
}
// 获取Entity对象
HttpEntity entity = response.getEntity();
// 获取响应信息流
InputStream in = null;
if (Objects.nonNull(entity)) {
try {
in = entity.getContent();
} catch (Exception e) {
e.printStackTrace();
}
}
return in;
}
/**
* 执行请求,获取响应内容
*
* @param request 请求对象
* @return 响应内容
*/
private static String getRespString(HttpUriRequest request, Map<String, String> headers) {
byte[] bytes = new byte[BYTE_ARRAY_LENGTH];
int len = 0;
try (InputStream in = getRespInputStream(request, headers);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
if (Objects.isNull(in)) {
return "";
}
while ((len = in.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
return bos.toString(UTF8);
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
maven引入:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<!-- HttpClinet 核心包 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>