个人开发者实现微信扫码登录
使用码上登录中转微信扫码登录
使用之前最好有一个公网服务器,能够公网访问的 redis 和 mysql 数据库,并且能够部署公网访问的服务
码上登录是一个小程序,对个体开发者提供了免费的微信扫一扫登录入口:官网 http://login.vicy.cn/
针对微信扫码登录需要进行企业认证,个人开发者无法使用的问题,我们可以使用码上登录进行桥接,将微信扫码登录的信息转发给我们的程序
在开发之前,我们需要有一个能被公网访问的地址,用来接收码上登录回调给我们的用户数据
更多信息参考码上登录 Api 文档:http://login.vicy.cn/apiWord.html
使用步骤
进入码上登录官网创建我们的应用:http://login.vicy.cn/
-
微信扫码登录,进入首页,点击
马上创建应用
-
点击创建应用,输入网站名称和回调 URL ,填写完成后创建回得到
secretKey网站名称就是用户扫码后看到的你的网站名称
回调 URL 就是用户确认登录后,接收用户信息的
controller,可以先随便写,再后期修改

创建后台应用
-
先创建一个
web模块service-ucenter用于编写代码,需要整合mybatis和redis,我使用SpringBoot的web项目导入
commons和对应的数据库依赖,该依赖的作用是让我们发送HTTP请求,获取二维码信息<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!--http相关依赖--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>配置文件如下,拿去使用记得修改对应的数据库配置
server: port: 8085 servlet: application-display-name: service-ucenter spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://IP:3306/guli_college?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: root # 配置 druid 连接池 type: com.alibaba.druid.pool.DruidDataSource redis: host: IP port: 6379 database: 0 timeout: 1800000 mvc: pathmatch: matching-strategy: ant_path_matcher # 开启rest风格 hiddenmethod: filter: enabled: true -
编写
HttpClientUtils工具类,直接粘过去用package com.zym.utils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.Consts; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig.Builder; 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.conn.ConnectTimeoutException; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicNameValuePair; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import java.io.IOException; import java.net.SocketTimeoutException; import java.security.GeneralSecurityException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * 依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar * @author zhaoyb * */ public class HttpClientUtils { public static final int connTimeout=10000; public static final int readTimeout=10000; public static final String charset="UTF-8"; private static HttpClient client = null; static { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(128); cm.setDefaultMaxPerRoute(128); client = HttpClients.custom().setConnectionManager(cm).build(); } public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{ return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout); } public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{ return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout); } public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException, SocketTimeoutException, Exception { return postForm(url, params, null, connTimeout, readTimeout); } public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { return postForm(url, params, null, connTimeout, readTimeout); } public static String get(String url) throws Exception { return get(url, charset, null, null); } public static String get(String url, String charset) throws Exception { return get(url, charset, connTimeout, readTimeout); } /** * 发送一个 Post 请求, 使用指定的字符集编码. * * @param url * @param body RequestBody * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3 * @param charset 编码 * @param connTimeout 建立链接超时时间,毫秒. * @param readTimeout 响应超时时间,毫秒. * @return ResponseBody, 使用指定的字符集编码. * @throws ConnectTimeoutException 建立链接超时异常 * @throws SocketTimeoutException 响应超时 * @throws Exception */ public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { HttpClient client = null; HttpPost post = new HttpPost(url); String result = ""; try { if (StringUtils.isNotBlank(body)) { HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset)); post.setEntity(entity); } // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } post.setConfig(customReqConf.build()); HttpResponse res; if (url.startsWith("https")) { // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(post); } else { // 执行 Http 请求. client = HttpClientUtils.client; res = client.execute(post); } result = IOUtils.toString(res.getEntity().getContent(), charset); } finally { post.releaseConnection(); if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } return result; } /** * 提交form表单 * * @param url * @param params * @param connTimeout * @param readTimeout * @return * @throws ConnectTimeoutException * @throws SocketTimeoutException * @throws Exception */ public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { HttpClient client = null; HttpPost post = new HttpPost(url); try { if (params != null && !params.isEmpty()) { List<NameValuePair> formParams = new ArrayList<NameValuePair>(); Set<Entry<String, String>> entrySet = params.entrySet(); for (Entry<String, String> entry : entrySet) { formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); post.setEntity(entity); } if (headers != null && !headers.isEmpty()) { for (Entry<String, String> entry : headers.entrySet()) { post.addHeader(entry.getKey(), entry.getValue()); } } // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } post.setConfig(customReqConf.build()); HttpResponse res = null; if (url.startsWith("https")) { // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(post); } else { // 执行 Http 请求. client = HttpClientUtils.client; res = client.execute(post); } return IOUtils.toString(res.getEntity().getContent(), "UTF-8"); } finally { post.releaseConnection(); if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } } /** * 发送一个 GET 请求 * * @param url * @param charset * @param connTimeout 建立链接超时时间,毫秒. * @param readTimeout 响应超时时间,毫秒. * @return * @throws ConnectTimeoutException 建立链接超时 * @throws SocketTimeoutException 响应超时 * @throws Exception */ public static String get(String url, String charset, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,SocketTimeoutException, Exception { HttpClient client = null; HttpGet get = new HttpGet(url); String result = ""; try { // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } get.setConfig(customReqConf.build()); HttpResponse res = null; if (url.startsWith("https")) { // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(get); } else { // 执行 Http 请求. client = HttpClientUtils.client; res = client.execute(get); } result = IOUtils.toString(res.getEntity().getContent(), charset); } finally { get.releaseConnection(); if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } return result; } /** * 从 response 里获取 charset * * @param ressponse * @return */ @SuppressWarnings("unused") private static String getCharsetFromResponse(HttpResponse ressponse) { // Content-Type:text/html; charset=GBK if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) { String contentType = ressponse.getEntity().getContentType().getValue(); if (contentType.contains("charset=")) { return contentType.substring(contentType.indexOf("charset=") + 8); } } return null; } /** * 创建 SSL连接 * @return * @throws GeneralSecurityException */ private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException { try { SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() { @Override public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { return true; } }).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() { @Override public boolean verify(String arg0, SSLSession arg1) { return true; } @Override public void verify(String host, SSLSocket ssl) throws IOException { } @Override public void verify(String host, X509Certificate cert) throws SSLException { } @Override public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { } }); return HttpClients.custom().setSSLSocketFactory(sslsf).build(); } catch (GeneralSecurityException e) { throw e; } } } -
编写
controller和service代码,处理前端操作,获取二维码信息Controller代码/** * 微信扫码登录 * @author zym */ @RestController @CrossOrigin @RequestMapping("/wx") public class WxLogin { @Resource private MemberService memberServiceImpl; @Resource private RedisTemplate redisTemplate; /** * 向码上登录服务器请求二维码信息 * @return */ @GetMapping("/login") public String login(HttpSession session) throws GuLiError { return memberServiceImpl.getQrCode(); } }编写
Service代码,通过HttpClentUtils发送HTTP请求,得到用户扫码登录的二维码信息请求地址为:
https://server01.vicy.cn/8lXdSX7FSMykbl9nFDWESdc6zfouSAEz/wxlogin/wxLogin/tempUserId?secretKey=你的secretKey将
secretKey更改为自己刚刚在码上登录创建应用得到的secretKey返回值
codeUrl里面存放了二维码信息,具体参数如下:{"errcode":0,"data":{"qrCodeReturnUrl":"http://login.vicy.cn?tempUserId=18d55e78f66848e781bc7ce65a1fe5b4", "tempUserId":"18d55e78f66848e781bc7ce65a1fe5b4"},"message":"成功"}属性 类型 说明 qrCodeReturnUrl string 把qrCodeReturnUrl发送给前端生成二维码 tempUserId string 该次登录会话的凭证,一个二维码的 tempUserId 在回调的时候会传递给后端 /** * @author zym */ @Service public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService { @Resource private RedisTemplate redisTemplate; /** * 通过码上登录获取二维码信息 * @return */ @Override public String getQrCode() { String codeUrl = null; try { codeUrl = HttpClientUtils.get("https://server01.vicy.cn/8lXdSX7FSMykbl9nFDWESdc6zfouSAEz/wxlogin/wxLogin/tempUserId?secretKey=06a0b50645544c58ba32cb475ae6330a"); } catch (Exception e) { e.printStackTrace(); } return codeUrl; } }
前端展示二维码
使用 vue-qriously 利用二维码信息 qrCodeReturnUrl 生成二维码,展示给用户,代码如下
<template>
<div class="main">
<div v-show="!showform">
<qriously :value="qrCodeReturnUrl" :size="250"/>
<font aligen="center">请使用微信扫码登录</font>
<!-- initQCode: 是你在你的vue实例中定义好的变量 size:是这个Canvas的大小,这里要根据你的视觉稿来决定-->
</div>
<!-- 更多登录方式 -->
<div class="more-sign">
<h6>社交帐号登录</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" @click="getQrCodeReturnUrl()"><i
class="iconfont icon-weixin" /></a></li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
import cookie from 'js-cookie'
import register from '@/api/register'
import Qriously from 'vue-qriously'
export default {
layout: 'sign',
data () {
return {
loginInfo:{},
qrCodeReturnUrl:'',
tempUserId:'',
showform:true
}
},
methods: {
// 获取扫码登录信息
getQrCodeReturnUrl(){
// 请求刚刚写的控制器 http://localhost:8085/wx/login,可以直接 ajax 异步调用,这里使用 vue-admin 模板作了封装
register.getQrCodeReturnUrl().then(response => {
if (response.data.errcode === 0){
console.log(response.data.data);
this.qrCodeReturnUrl = response.data.data.qrCodeReturnUrl;
this.tempUserId = response.data.data.tempUserId
this.showform = false;
}
})
}
}
</script>
<style>
.el-form-item__error{
z-index: 9999999;
}
</style>
编写码上登录传递信息给我们的地址
同样在上面的 controller 中添加回调方法,此方法必须是 post 请求类型,接收如下五个参数
| 属性 | 类型 | 说明 |
|---|---|---|
| userId | string | 用户唯一ID |
| tempUserId | string | 该次会话的凭证 |
| nickname | string | 登录用户的昵称(用户授权后,可获得,可能为空) |
| avatar | string | 登录用户的头像url(用户授权后,可获得,可能为空) |
| ipAddr | string | 登录地点的ip |
该方法还需要返回扫码登录状态,码上登录会根据返回的信息在用户手机上提示是否登录成功,返回信息如下,使用 map 封装这两个属性,需要将其转换成 json 格式进行返回
| 属性 | 类型 | 说明 |
|---|---|---|
| errcode | number | 错误码(0:请求成功) |
| message | string | 接口调用情况描述,如:”成功” |
详细代码如下:注意:此方法需要解决跨域问题,我使用了 Getway 网关解决,也可以加上 @CrossOrigin 注解或者其他方式
/**
* 微信登录回调
* @return
*/
@PostMapping("/callback")
@ResponseBody
public String callback(@RequestParam(value = "userId",required = false) String userId,
@RequestParam(value = "tempUserId",required = false) String tempUserId,
@RequestParam(value = "nickname",required = false) String nickname,
@RequestParam(value = "avatar",required = false) String avatar,
@RequestParam(value = "ipAddr",required = false) String ipAddr,
HttpServletResponse httpServletResponse) {
try {
// 判断用户是否第一次登录
Member memberById = memberServiceImpl.getById(userId);
if (memberById == null) {
// 第一次登录,创建用户
Member member = new Member();
member.setId(userId);
member.setNickname(nickname);
member.setAvatar(avatar);
memberServiceImpl.save(member);
memberById = memberServiceImpl.getById(userId);
}
// 使用 JwtUtils 生成 token
String token = JwtUtils.getJwtToken(memberById.getId(), memberById.getNickname());
// 将用户 token 存放进 redis,键是 tempUserId
redisTemplate.opsForValue().set(tempUserId, token, 1, TimeUnit.MINUTES);
HashMap<String, Object> map = new HashMap<>(1);
if (userId != null) {
map.put("errcode", 0);
map.put("message", "登录成功");
}
Gson gson = new Gson();
return gson.toJson(map);
} catch (Exception e) {
HashMap<String, Object> map = new HashMap<>(1);
map.put("errcode", 1);
map.put("message", "登录失败");
Gson gson = new Gson();
return gson.toJson(map);
}
}
JwtUtils 如下:
package com.zym.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.Optional;
/**
* JWT工具类
* @author zym
*/
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24;
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("guli_token");
if(!Optional.ofNullable(jwtToken).isPresent()) {
return "";
}
Jws<Claims> claimsJws =
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}
将本项目打包放到公网服务器上运行
登录码上登录官网,修改之前创建应用的回调 URL 为 callback 方法的公网请求路径

前端获取登录状态
在之前创建的 controller 中新增 getToken 方法,根据 tempUserId 循环查询 redis 中是否有这个用户的信息,如果有,就将其返回给前端
一段时间后,redis 都没有查询到用户的 token 则视为登录过期,提示用户刷新二维码后重新登录
/**
* 微信登录回调,根据用户 tempUserId 从 redis 中获取 token
* @return
*/
@GetMapping("/getToken/{tempUserId}")
public R getToken(@PathVariable String tempUserId) {
System.out.println("tempUserId = " + tempUserId);
boolean flag = false;
for (int i = 0; i < 60; i++) {
String token = (String) redisTemplate.opsForValue().get(tempUserId);
if (token != null) {
return R.ok().setData("token", token);
}else {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return R.error().setData("message", "登录超时,点击微信图标刷新二维码后重试");
}
前端编写 getToken 函数,调用这个 controller 查询登录状态,这里只展示 js 代码
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
import cookie from 'js-cookie'
import register from '@/api/register'
import Qriously from 'vue-qriously'
export default {
layout: 'sign',
data () {
return {
loginInfo:{},
qrCodeReturnUrl:'',
tempUserId:'',
showform:true
}
},
methods: {
// 获取扫码登录信息
getQrCodeReturnUrl(){
register.getQrCodeReturnUrl().then(response => {
if (response.data.errcode === 0){
console.log(response.data.data);
this.qrCodeReturnUrl = response.data.data.qrCodeReturnUrl;
this.tempUserId = response.data.data.tempUserId
this.showform = false;
// 调用 getToken 函数
this.getToken();
}
})
},
// 获取扫码登录用户token
getToken(){
// 请求后端刚刚写的控制器 http://localhost:8085/wx/getToken/这里是tempUserId ,查询这个二维码的登录信息
register.getToken(this.tempUserId).then(response => {
// 成功获取到登录信息,则将用户 token 保存到 cookie 中,并跳转到网站首页
if (response.data.status === true) {
console.log(response.data.data);
cookie.set("token", response.data.data.token);
this.$router.push("/");
} else {
// 获取用户信息超时的逻辑请自己编写
}
})
}
},
}
</script>
注意:这里使用jwt做单点登录,你也可以直接存储用户信息