JFinal使用技巧-登录功能之 微信二维码登录

应波大之邀分享一篇 微信公众号扫码登录网站的功能。
本次分享的技术方案是纯微信带参二维码的使用,并非微信平台自带的 微信网页开发 /网页授权 功能。
服务号必须通过微信认证的。开发测试使用测试号:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo

好了,废话不多说上码!

本次分享还是使用 jfinal_demo 做一个演示流程!
下载jfinal_demo不用再说了 https://jfinal.com/download/now?file=jfinal_demo_for_maven-5.0.0.zip
直接上改动部分:
1、添加微信 SDK 依赖:

<!-- 微信公众号SDK -->
<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal-weixin</artifactId>
    <version>3.4</version>
</dependency>
<!-- JSON 工具可以使用其他的,这里先放fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

2、创建一个 Account 表:账户密码啥的字段就先不加了,主要是模拟微信openId的使用:

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `openId` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

再执行  _JFinalDemoGenerator 工具,生成 Account 类

3、增加一个用户登录拦截器

package com.demo.index;

import com.demo.common.model.Account;
import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 用户登录拦截器
 */
public class LogonAccountInterceptor implements Interceptor {
    public static final String KEY_LOGON_TOKEN = "logon_token";
    // 测试代码 使用 map 当登录用户缓存,大家一般使用 CacheKit 之类的缓存来处理
    private static final Map<String, Account> logonAccountCache = new ConcurrentHashMap<>();

    @Override
    public void intercept(Invocation inv) {
        Account logonAccount = get(inv.getController());
        if (logonAccount != null){
            inv.invoke();
        }else{
            // frp代理的原因未能识别域名,这里测试先写死
            String base = "http://dfz.a.myedu.top";
            inv.getController().redirect(base + "/weixin/api/logonOAuth");
        }
    }
    public static void put(String token, Account logonAccount){
        logonAccountCache.put(token, logonAccount);
    }

    public static Account get(Controller c) {
        String token = c.getCookie(KEY_LOGON_TOKEN);
        if (token == null) return null;
        return logonAccountCache.get(token);

        // 虚拟一个登录用户信息 用于测试
//        Account logonAccount = Account.temp;
//        if (logonAccount == null){
//            logonAccount = Account.dao.findById(1);
//            if (logonAccount == null){
//                logonAccount = new Account();
//                logonAccount.setId(1);
//                logonAccount.setName("dufuzhong");
//                logonAccount.save();
//            }
//            Account.temp = logonAccount;
//        }
//        return logonAccount;
    }

}

上面代码 使用 map 当登录用户缓存,大家一般都是使用 CacheKit 之类的缓存来处理吧,这里先模拟一下,不加其他依赖了。

4、增加一个生成二维码的调用功能

package com.demo.weixin;

import com.demo.common.model.Account;
import com.demo.index.LogonAccountInterceptor;
import com.jfinal.aop.Clear;
import com.jfinal.core.Path;
import com.jfinal.kit.Ret;
import com.jfinal.kit.StrKit;
import com.jfinal.weixin.sdk.api.*;
import com.jfinal.weixin.sdk.jfinal.ApiController;

//多微信公众号需要使用ApiInterceptor
//@Before(ApiInterceptor.class)
@Path("/weixin/api")
public class WeiXinApiController extends ApiController {

    /**
     * 登录二维码网页
     * /weixin/api/logonOAuth
     */
    @Clear(LogonAccountInterceptor.class)
    public void logonOAuth(){
        String event = get("event");
        if ("showQrcodeUrl".equals(event)){
            String token = StrKit.getRandomUUID();
            setCookie("logon_token", token, -1, true);
            String sceneStr = "logonOAuth:" + token;
            ApiResult apiResult = QrcodeApi.createTemporary(30 * 15, sceneStr);
            String showQrcodeUrl = QrcodeApi.getShowQrcodeUrl(apiResult.getStr("ticket"));
            redirect(showQrcodeUrl);
            return;
        }
        if ("check".equals(event)){
            Account logonAccount = LogonAccountInterceptor.get(this);
            renderJson(logonAccount != null ? Ret.ok() : Ret.fail());
            return;
        }
        render("/weixin/login.html");
    }

    /**
     * 登录人绑定二维码
     * /weixin/api/logonAccountBindingOAuth
     */
    public void logonAccountBindingOAuth(){
        // 模拟登录人
        String event = get("event");
        Account logonAccount = Account.temp;
        if ("showQrcodeUrl".equals(event)) {
            // 推荐使用account增加一个 uuid 字段或用缓存uuid映射id做为唯一标识,这里简单使用 id 标识
            String sceneStr = "logonAccountBindingOAuth:" + logonAccount.getId();
            ApiResult apiResult = QrcodeApi.createTemporary(30 * 15, sceneStr);
            String showQrcodeUrl = QrcodeApi.getShowQrcodeUrl(apiResult.getStr("ticket"));
            redirect(showQrcodeUrl);
            return;
        }
        set("logonAccount", logonAccount);
        render("/weixin/logonAccountBindingOAuth.html");
    }


}

上面logonOAuth代码有使用Cookie。
如果是分离项目,就用json 返回token、showQrcodeUrl 值,登录拦截器通过getHeader取token即可。

5、增加两个界面 一个绑定,一个登录:

image.png

login.html

#@layout()
#define main()
<h1>微信扫码登录</h1>
<img src="/weixin/api/logonOAuth?event=showQrcodeUrl">
<p id="msg"></p>
<script>
$(document).ready(function(){
    var count = 30 * 15;
    function check() {
        $("#msg").html("二维码失效到计时 "+ count +" 秒");
        count --;
        $.getJSON("/weixin/api/logonOAuth?event=check", function(ret){
            console.log(ret);
            if(ret.state == "ok") {
                window.location.href = '/';
            }else {
                setTimeout(function(){ check() }, 1000);
            }
        });
    }
    setTimeout(function(){ check() }, 3000);
});
</script>
#end

logonAccountBindingOAuth.html

#@layout()
#define main()
<h1>微信扫码绑定 #(logonAccount.name)</h1>
<img src="/weixin/api/logonAccountBindingOAuth?event=showQrcodeUrl">
#end

index.html 增加一个按钮入口吧,正常应该是有个人中心 然后有个微信绑定按钮、手机、邮箱之类的绑定。

public void index() {
    set("logonAccount", LogonAccountInterceptor.get(this));
    render("index.html");
}
省略代码...
账户微信绑定:
【#(logonAccount.openId ? '已绑定':'未绑定')】
<a href="/weixin/api/logonAccountBindingOAuth"><b>#(logonAccount.openId ? '重新绑定':'点击绑定')</b></a>


6、增加微信消息回调处理:

package com.demo.weixin;

import com.demo.common.model.Account;
import com.demo.index.LogonAccountInterceptor;
import com.jfinal.aop.Before;
import com.jfinal.aop.Clear;
import com.jfinal.core.Path;
import com.jfinal.weixin.sdk.jfinal.MsgControllerAdapter;
import com.jfinal.weixin.sdk.jfinal.MsgInterceptor;
import com.jfinal.weixin.sdk.msg.in.InTextMsg;
import com.jfinal.weixin.sdk.msg.in.event.InFollowEvent;
import com.jfinal.weixin.sdk.msg.in.event.InMenuEvent;
import com.jfinal.weixin.sdk.msg.in.event.InQrCodeEvent;
import com.jfinal.weixin.sdk.msg.out.OutTextMsg;

@SuppressWarnings("unused")
@Path("/weixin/msg")
public class WeiXinMsgController extends MsgControllerAdapter {
    @Override
    @Clear
    @Before(MsgInterceptor.class)
    public void index() {
        super.index();
    }

    
    /**
     * @param event 处理接收到的二维码扫描事件
     */
    @Override
    protected void processInQrCodeEvent(InQrCodeEvent event) {
        String eventKey = event.getEventKey();
        String openId = event.getFromUserName();
        System.out.println("processInQrCodeEvent=" + eventKey);
    
        String logonAccountId = getValByEventKey(eventKey, "logonAccountBindingOAuth:");
        if (StrKit.notBlank(logonAccountId)){
            logonAccountBindingOAuth(event, openId, logonAccountId);
            return;
        }
        String logonToken = getValByEventKey(eventKey, "logonOAuth:");
        if (StrKit.notBlank(logonToken)){
            logonOAuth(event, openId, logonToken);
            return;
        }
    
        OutTextMsg outMsg = new OutTextMsg(event);
        outMsg.setContent("感谢您的关注!" + eventKey);
        render(outMsg);
    }
    
    private String getValByEventKey(String eventKey, String key) {
        if (StrKit.isBlank(eventKey)){ return null; }
        if (eventKey.startsWith(key)){
            return eventKey.substring(key.length());
        }
        String first = "qrscene_";
        if (eventKey.startsWith(first)){
            // 第一次扫码关注 参数会带前缀,切掉继续匹配
            return getValByEventKey(eventKey.substring(first.length()), key);
        }
        return null;
    }
    
    private void logonOAuth(InQrCodeEvent event, String openId, String token) {
        String sql = "SELECT * FROM account a WHERE a.openId = ? LIMIT 1";
        Account logonAccount = Account.dao.findFirst(sql, openId);
        if (logonAccount == null){
            // 这个位置看自己业务, 是否需要游客模式,如果 account 没有记录,就生成一条数据,返回登录成功就可以
            OutTextMsg outMsg = new OutTextMsg(event);
            outMsg.setContent("登录失败");
            render(outMsg);
            return;
        }
        // 登录缓存对象
        LogonAccountInterceptor.put(token, logonAccount);
        OutTextMsg outMsg = new OutTextMsg(event);
        outMsg.setContent("登录成功!" + logonAccount.getName());
        render(outMsg);
    }
    
    private void logonAccountBindingOAuth(InQrCodeEvent event, String openId, String logonAccountId){
        Account logonAccount = Account.dao.findById(logonAccountId);
        if (logonAccount == null){
            OutTextMsg outMsg = new OutTextMsg(event);
            outMsg.setContent("绑定失败");
            render(outMsg);
            return;
        }
        logonAccount.setOpenId(openId);
        logonAccount.update();
        //登录人绑定二维码
        OutTextMsg outMsg = new OutTextMsg(event);
        outMsg.setContent("成功绑定用户:" + logonAccount.getName());
        render(outMsg);
    }

    @Override
    protected void processInFollowEvent(InFollowEvent inFollowEvent) { }
    @Override
    protected void processInTextMsg(InTextMsg inTextMsg) { }
    @Override
    protected void processInMenuEvent(InMenuEvent inMenuEvent) { }
}

通过切割eventKey事件值 进行区分 不同的扫码事件进行业务处理即可。

7、配置DemoConfig ,临时可以使用微信测试号 https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo

public void configInterceptor(Interceptors me) {
       //登录用户 拦截器
       me.add(new LogonAccountInterceptor());
}

@Override
public void onStart() {
    ApiConfig ac = new ApiConfig();
    ac.setToken("jfinal");
    ac.setAppId("xxx");
    ac.setAppSecret("xxx");
    ApiConfigKit.putApiConfig(ac);
    ApiConfigKit.setDevMode(true);
}


8、启动内网穿透和 demo演示一下吧!

我们自己有公网服务器,所以使用 frps + nignx + frpc 感觉更方便一些,域名流量啥的套餐都不限制了。
(阿里云个人主机一年 99 可以看下,再装个1panel面板https://1panel.cn,在商店一键安装frps也很简单)

如果没有自己的服务器,就使用https://natapp.cn/ 这个软件吧,附优惠码:C01EC183

image.png

PS:我有修改端口为 8080

需要先调通 内网穿透代理 访问,再测试了。不然微信消息不能回调过来。二维码加载慢是因为代理中间转的慢,正常在服务器上还是很快的。

image.png

配置成功时,控制台有输出成功消息


image.png

打开微信扫一扫,收到消息登录失败,是因为我还没绑定了。没有做没绑定的微信自动添加为新用户。

image.png

在登录拦截器位置先处理一下,先写死为 id 为 1 的数据(数据库中添加了 1 条,表示旧数据),上面代码有注释,放开注释并注释前面 2 行即可。

    public static Account get(Controller c) {
//        String token = c.getCookie(KEY_LOGON_TOKEN);
//        if (token == null) return null;
//        return logonAccountCache.get(token);

//         虚拟一个登录用户信息 用于测试
        Account logonAccount = Account.temp;
        if (logonAccount == null){
            logonAccount = Account.dao.findById(1);
            if (logonAccount == null){
                logonAccount = new Account();
                logonAccount.setId(1);
                logonAccount.setName("dufuzhong");
                logonAccount.save();
            }
            Account.temp = logonAccount;
        }
        return logonAccount;
    }

重启再 直接登录成功,然后再点绑定试试:

image.png

image.png

image.png

再看下数据库:

    image.png

奈斯~

再去掉 拦截器写死的代码,并重启 JFinal

public static Account get(Controller c) {
    String token = c.getCookie(KEY_LOGON_TOKEN);
    if (token == null) return null;
    return logonAccountCache.get(token);
}


image.png

image.png

image.png

image.png


搞定~ 时间仓促没有过多美化内容,流程为主了



评论区

JFinal

2024-10-09 10:44

正好要用上,这回省事了,感谢感谢, 点赞 + 收藏

jfinal009

2024-10-10 14:44

@杜福忠 请教,如果第一次扫码,未关注公众号;可否实现扫码一次,实现关注及登录,如果关注过,直接登录。

杜福忠

2024-10-10 16:06

@jfinal009 可以的,我修改一下processInQrCodeEvent 事件的判断方式(上面内容已经更新),第一次扫码带关注事件值会携带一个qrscene_前缀,处理掉就就可以了

jfinal009

2024-10-10 17:18

@杜福忠 赞,查询了微信文档,这个处理方式优雅。谢谢。还有个问题,多用户登录时 Account.temp 会造成冲突;已经用了 Map 为什么还用Account.temp存储呢?

杜福忠

2024-10-10 18:05

@jfinal009 Account.temp 是个临时模拟静态值,因为要模拟:登录成功》绑定账户 这个流程。没加登录模块,先写死一个静态变量值方便模拟 登录成功的状态。
代码是昨晚临时写的,只能作为一个操作流程参考,对接扫码登录功能到自己的业务系统。有空看下做一个完整的代码包上到社区app模块试试

jfinal009

2024-10-10 18:18

@杜福忠 谢谢分享,期待上传App&Coffee,好有机会请您喝杯哈

北流家园网

2024-10-12 11:56

收藏先

杜福忠

2024-10-20 16:16

@jfinal009 上架 app 栏 https://jfinal.com/app/10056

jfinal009

2024-10-21 09:17

@杜福忠 赞,果断拍下

热门分享

扫码入社