搜索
您的当前位置:首页正文

使用ThinkJs搭建微信中控服务的实现方法

2023-12-06 来源:好心意情感网

本人前端渣渣一枚,这篇文章是第一次写,如果有硬核bug,请大佬们轻喷、指出... 另外,本文不涉及任何接口安全、参数校验之类的东西,默认对调用方无脑级的信任:joy: 目前自用的接口包括但不限于以下这些

|--- 微信相关| |--- 0. 处理微信推过来的一些消息| |--- 1. 获取微信SDK配置参数| |--- 2. 微信鉴权登陆| |--- 3. 获取微信用户信息| |--- 4. 获取AccessToken| |--- 5. 批量发送模版消息| |--- 6. 获取模版消息列表| |--- 7. 批量发送客服消息

背景

  • 【需求】小项目很多很杂,而且大部分需求都是基于微信开发的,每次都查微信文档的话就会很郁闷:unamused:...
  • 【号多】公众号超级多,项目中偶尔会涉及借权获取用户信息(在不绑定微信开放平台的前提下,需要临时自建各个公众号的openid关联关系),类似这样同时需要不止一个公众号配合来完成一件事的需求,就容易把人整懵逼...
  • 【支付】微信支付的商户号也很多,而且有时候支付需要用的商户号,还不能用关联的公众号取出来的openid去支付...
  • 【官方】微信官方文档建议!把获取AccessToken等微信API抽离成单独的服务... 等等等等........所以...:joy:
  • 创建ThinkJS项目

    官网

    thinkjs.org/

    简介

    ThinkJS 是一款面向未来开发的 Node.js 框架,整合了大量的项目最佳实践,让企业级开发变得如此简单、高效。从 3.0 开始,框架底层基于 Koa 2.x 实现,兼容 Koa 的所有功能。

    安装脚手架

    $ npm install -g think-cli

    创建及启动项目

    $ thinkjs new demo;$ cd demo;$ npm install; $ npm start; 

    目录结构

    |--- development.js //开发环境下的入口文件|--- nginx.conf //nginx 配置文件|--- package.json|--- pm2.json //pm2 配置文件|--- production.js //生产环境下的入口文件|--- README.md|--- src| |--- bootstrap //启动自动执行目录 | | |--- master.js //Master 进程下自动执行| | |--- worker.js //Worker 进程下自动执行| |--- config //配置文件目录| | |--- adapter.js // adapter 配置文件 | | |--- config.js // 默认配置文件 | | |--- config.production.js //生产环境下的默认配置文件,和 config.js 合并 | | |--- extend.js //extend 配置文件 | | |--- middleware.js //middleware 配置文件 | | |--- router.js //自定义路由配置文件| |--- controller //控制器目录 | | |--- base.js| | |--- index.js| |--- logic //logic 目录| | |--- index.js| |--- model //模型目录| | |--- index.js|--- view //模板目录| |--- index_index.html

    安装think-wechat插件

    介绍

    微信中间件,基于 node-webot/wechat,支持 thinkJS 3.0

    安装

    $ npm install think-wechat --save

    $ cnpm install think-wechat --save

    配置

    文件:/src/config/middleware.js

    const wechat = require('think-wechat')module.exports = [ ... { handle: wechat, match: '/index', options: { token: '', // 令牌,和公众号/基本配置/服务器配置里面写一样的即可 appid: '', // 这里貌似可以随便填,因为我们后面要用数据库配置多个公众号 encodingAESKey: '', checkSignature: false } }, { handle: 'payload', // think-wechat 必须要在 payload 中间件前面加载,它会代替 payload 处理微信发过来的 post 请求中的数据。 options: { keepExtensions: true, limit: '5mb' } },]

    注:match下我这里写的是 /index ,对应的项目文件是 /src/controller/index.js ,对应的公众号后台所需配置的服务器地址就是 http(https)://域名:端口/index

    创建数据库和相关表

    我这里创建了三个微信的相关表。

    配置表:wx_config

    字段 类型 说明
    id int 主键
    name varchar 名称
    appid varchar appid
    secret varchar secret

    用户表:wx_userinfo

    字段 类型 注释
    id int 主键
    subscribe int 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。
    nickname varchar 用户的昵称
    sex int 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
    language varchar 用户所在省份
    city varchar 用户所在城市
    province varchar 用户所在省份
    country varchar 用户所在国家
    headimgurl longtext 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
    subscribe_time double 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
    unionid varchar 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
    openid varchar 用户的标识,对当前公众号唯一
    wx_config_id int 对应配置的微信号id

    模版消息日志表:wx_template_log

    字段 类型 注释
    id int 主键
    template_id varchar 模版id
    openid varchar 用户的标识,对当前公众号唯一
    url varchar 跳转url
    miniprogram varchar 跳转小程序
    data varchar 发送内容json字符串
    add_time double 添加时间戳
    send_time double 发送时间戳
    send_status varchar 发送结果
    wx_config_id double 对应配置的微信号id
    uuid varchar 本次发送的uuid,业务系统可通过uuid查询模版消息推送结果

    处理微信推送消息

    文件目录

    /src/controller/index.js

    文件内容

    module.exports = class extends think.Controller { /* * 入口:验证开发者服务器 * 验证开发者服务器,这里只是演示,所以没做签名校验,实际上应该要根据微信要求进行签名校验 */ async indexAction() { let that = this; if (that.method != 'REPLY') { return that.json({code: 1, msg: '非法请求', data: null}) } const {echostr} = that.get(); return that.end(echostr); } /* * 文字 * 用于处理微信推过来的文字消息 */ async textAction() { let that = this; let {id, signature, timestamp, nonce, openid} = that.get(); let {ToUserName, FromUserName, CreateTime, MsgType, Content, MsgId} = that.post(); ..... that.success('') } /* * 事件 * 用于处理微信推过来的事件消息,例如点击菜单等 */ async eventAction() { let that = this; let {id, signature, timestamp, nonce, openid} = that.get(); let {ToUserName, FromUserName, CreateTime, MsgType, Event, EventKey, Ticket, Latitude, Longitude, Precision} = that.post(); switch (Event) { case 'subscribe': // 关注公众号 ... break; case 'unsubscribe': // 取消关注公众号 ... break; case 'SCAN': // 已关注扫码 ... break; case 'LOCATION': // 地理位置 ... break; case 'CLICK': // 自定义菜菜单 ... break; case 'VIEW': // 跳转 ... break; case 'TEMPLATESENDJOBFINISH':// 模版消息发送完毕 ... break; } that.success('') }}

    注:支持的action包括: textActionimageActionvoiceActionvideoActionshortvideoActionlocationActionlinkActioneventActiondeviceTextActiondeviceEventAction

    公众号后台配置

    注:后面跟的id参数是为了区分是哪个公众号推过来的消息,在上面的接口参数中也有体现

    微信相关API的编写

    目录结构

    |--- src| |--- controller //控制器目录 | | |--- index.js // 处理微信推送的消息,上面有写到| | |--- common.js // 一些公共方法| | |--- open // 开放给其他业务服务的api接口| | | |--- wx.js| | |--- private // 放一些内部调用的方法,调用微信api的方法主要在这里面| | | |--- wx.js

    这个目录结构可能不太合理,后期再改进吧:grin:

    公共方法

    // src/controller/common.jsimport axios from 'axios'import {baseSql} from "./unit";module.exports = class extends think.Controller { // 获取appinfo async getWxConfigById(id) { let that = this; let data = await that.cache(`wx_config:wxid_${id}`, async () => { // 数据库内取 let info = await that.model('wx_config', baseSql).where({id: id}).find(); if (!think.isEmpty(info)) { return info } }) return data || {} } // 获取access_token async getAccessToken(id) { let that = this; let accessToken = await that.cache(`wx_access_token:wxid_${id}`, async () => { let {appid, secret} = await that.getWxConfigById(id); let {data} = await axios({ method: 'get', url: `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}` }); return data.access_token }); return accessToken }}

    接口过滤器

    所有开放出来的接口的前置方法,俗称过滤器?所有开放的接口必传get参数是 wxid ,对应数据库表wx_config里面 id

    // src/controller/open/wx.jsasync __before() { let that = this; let wxid = that.get('wxid'); if (think.isEmpty(wxid)) { return that.json({code: 1, msg: 'wxid不存在'}) } that.wxConfig = await that.controller('common').getWxConfigById(wxid); if (think.isEmpty(that.wxConfig)) { return that.json({code: 1, msg: 'wxid不存在'}) }}

    接口 - 获取AccessToken

    代码

    // src/controller/open/wx.jsasync get_access_tokenAction() { let that = this; let accessToken = await that.controller('common').getAccessToken(that.wxConfig.id); return that.json({code: 0, msg: '', data: {access_token: accessToken}})}

    文档

     

    接口 - 获取微信sdk的config

    代码

    // src/controller/open/wx.jsasync get_wxsdk_configAction() { let that = this; let {url} = that.get(); if (think.isEmpty(url)) { return that.json({code: 1, msg: '参数不正确'}) } let sdkConfig = await that.controller('private/wx').getSdkConfig(that.wxConfig.id, url); return that.json({code: 0, msg: '', data: sdkConfig})}// src/controller/private/wx.jsconst sha1 = require('sha1');const getTimestamp = () => parseInt(Date.now() / 1000)const getNonceStr = () => Math.random().toString(36).substr(2, 15)const getSignature = (params) => sha1(Object.keys(params).sort().map(key => `${key.toLowerCase()}=${params[key]}`).join('&'));async getSdkConfig(id, url) { let that = this; let {appid} = await that.controller('common').getWxConfigById(id); let shareConfig = { nonceStr: getNonceStr(), jsapi_ticket: await that.getJsapiTicket(id), timestamp: getTimestamp(), url: url } return { appId: appid, timestamp: shareConfig.timestamp, nonceStr: shareConfig.nonceStr, signature: getSignature(shareConfig) }}

    文档

     

    接口 - 获取UserInfo

    代码

    // src/controller/open/wx.jsasync get_userinfoAction() { let that = this; let {openid} = that.get(); if (think.isEmpty(openid)) { return that.json({code: 1, msg: '参数不正确'}) } let userInfo = await that.controller('private/wx').getUserInfo(that.wxConfig.id, openid); if (think.isEmpty(userInfo)) { return that.json({code: 1, msg: 'openid不存在', data: null}) } return that.json({code: 0, msg: '', data: userInfo})}// src/controller/private/wx.jsasync getUserInfo(id, openid) { let that = this; let userInfo = await that.cache(`wx_userinfo:wxid_${id}:${openid}`, async () => { //先取数据库 let model = that.model('wx_userinfo', baseSql); let userInfo = await model.where({wx_config_id: id, openid: openid}).find(); if (!think.isEmpty(userInfo) && userInfo.subscribe == 1 && userInfo.unionid != null) { return userInfo } //如果数据库内没有,取新的存入数据库 let accessToken = await that.controller('common').getAccessToken(id); let url = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${accessToken}&openid=${openid}&lang=zh_CN`; let {data} = await axios({method: 'get', url: url}); if (data.openid) { //命中修改,没有命中添加 let resId = await model.thenUpdate( Object.assign(data, {wx_config_id: id}), {openid: openid, wx_config_id: id}); return await model.where({id: resId}).find(); } }) return userInfo}

    文档

     

    接口 - 批量发送文字客服消息

    代码

    // src/controller/open/wx.jsasync send_msg_textAction() { let that = this; let {list} = that.post(); if (think.isEmpty(list)) { return that.json({code: 1, msg: '参数不正确'}) } that._sendMsgTextList(that.wxConfig.id, list); return that.json({code: 0, msg: '', data: null}) } async _sendMsgTextList(wxid, list) { let that = this; let apiWxController = that.controller('private/wx'); for (let item of list) { let data = await apiWxController.sendMsgText(wxid, item.openid, item.text) }}// src/controller/private/wx.jsasync sendMsgText(id, openid, content) { let that = this; let accessToken = await that.controller('common').getAccessToken(id); let url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}` let {data} = await axios({ method: 'post', url: url, data: {"msgtype": 'text', "touser": openid, "text": {"content": content}} }) return data;}

    文档

     

    写在结尾

    其实还有很多接口,这里就不全部列出来了。

    应该能看出来,在这个项目里面并不仅仅是把微信的接口做了个简单的转发,而是有一些自己的处理逻辑在里面。

    比如获取微信用户信息的时候,会先判断缓存里有没有,如果没有就取数据库,如果还没有再去微信的接口取;如果数据库有,并且关注字段是未关注的话,还是会调用微信的接口取一波再更新。 反正一天内,微信接口的调用次数是绝对够用的。

    再比如批量发送模版消息,中控服务在收到请求后会先创建一个uuid,要发的模版消息全部保存到数据库内,直接把uuid返给调用方。 然后中控会异步用uuid取出来这批模版消息,一个一个发,一个一个更新结果。 这样在业务方调用发送模版消息之后,无需等待全部发送完毕,就可以用拿到的uuid,去中控查询这次批量发送的状态结果。

    目前是绑了七八个公众号,在没烧过香的前提下,还没出过什么问题

    小编还为您整理了以下内容,可能对您也有帮助:

    干货| WEB安全漏洞之SSRF

    什么是SSRF

    大家使用的服务中或多或少是不是都有以下的功能:

    1.通过URL地址分享内容。

    2.通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览,即所谓的转码功能。

    3.通过URL地址翻译对应文本的内容,即类似Google的翻译网页功能。

    4.通过URL地址加载或下载图片,即类似图片抓取功能。

    5.以及图片、文章抓取收藏功能。

    简单地说就是通过URL抓取其他服务器上数据然后做对应的操作的功能。以ThinkJS代码为例,我们的实现方法大概如下:

    本来是个不错的功能,但是当用户输入一个服务器可访问的内网地址,这个情况下它就会把内网的内容抓取出来展现给的用户。大多数公司会在内网中放置一些与公司相关的资料和关键数据,如果应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤,就可能存在这种服务端请求伪造的缺陷,即Server-Side Request Forgery,简称SSRF。

    SSRF的危害

    简单来说如果你的这个功能存在SSRF漏洞的话,相当于在攻击者和内网之间牵了根线,透过该功能攻击者可以间接访问到内网。攻击者可以利用SSRF实现的攻击主要有5种:

    1.可以对、服务器所在内网、本地进行端口扫描,获取一些服务的Banner信息。

    2.攻击运行在内网或本地的应用程序(比如溢出)。

    3.对内网Web应用进行指纹识别,通过访问默认文件实现。

    4.攻击内的Web应用,主要是使用GET参数就可以实现的攻击。

    5.利用file协议读取本地文件。

    其中最后一条的实现方式是用户输入file://本地文件协议地址,如果不作判断,程序很可能就会把本地文件读取出来返回给用户,例如file://etc/password服务器系统密码。

    防御方法

    首先我们需要禁用掉不需要的协议,仅允许HTTP(s)请求,防止最后一条使用file://等其他协议引起的问题,然后我们需要对输出内容进行判断,例如我应该输出一张图片,如果抓取返回来的是一段文本我们就不应该返回。以及如果抓取远端地址导致报错返回的情况,我们需要统一处理返回给用户的内容,而不是直接将远端服务器的内容返回给用户,这样让攻击者了解到了更多远端服务器的信息。

    除了输出内容的处理,我们还要对输入地址进行,过滤内网IP,访问内网行为。以之前的示例代码为例,正常我们会增加如下处理:

    短链接绕过

    大部分情况下这样处理是没有问题的,不过攻击者可不是一般人。这里存在一个两个可以绕过的方式,首先是短链接,短链接是先到短链接服务的地址之后再302跳转到真实服务器上,如果攻击者对内网地址进行短链处理之后以上代码会判断短链服务的IP为合法IP而通过校验。

    针对这种绕过方式,我们有两种方法来阻止:

    1.直接根据请求返回的响应头中的HOST来做内网IP判断。

    2.由于跳转后的地址也还是需要DNS解析的,所以只要在每次域名请求DNS解析处都做内网IP判断的逻辑即可。

    DNS重新绑定绕过

    另外一种绕过方式是利用DNS重绑定攻击。

    DNS如何重新绑定的工作

    攻击者注册一个域名,并在攻击者控制下将其代理给DNS服务器。服务器配置为很短响应时间的TTL记录,防止响应被缓存。当受害者浏览到恶意域时,攻击者的DNS服务器首先用托管恶意客户端代码的服务器的IP地址作出响应。例如,他们可以将受害者的浏览器指向包含旨在在受害者计算机上执行的恶意JavaScript或Flash脚本的网站。

    恶意客户端代码会对原始域名进行额外访问。这些都是由同源所允许的。但是,当受害者的浏览器运行该脚本时,它会为该域创建一个新的DNS请求,并且攻击者会使用新的IP地址进行回复。例如,他们可以使用内部IP地址或互联网上某个目标的IP地址进行回复。

    简单来说就是利用DNS服务器来使得每次解析返回不同的IP,当在校验IP的时候DNS解析返回合法的值,等后续重新请求内容的时候DNS解析返回内网IP。这种利用了多次DNS解析的攻击方式就是DNS重新绑定攻击。

    由于DNS重新绑定攻击是利用了多次解析,所以我们最好将校验和抓取两次DNS解析合并成一次,这里我们也有两种方法来阻止:

    1.将第一次DNS解析得到的IP直接用于第二次请求的DNS解析,去除第二次解析的问题。

    2.在抓取请求发起的时候直接判断解析的IP,如果不符合的话直接拒绝连接。

    针对以上解决方法,有开发者直接封装了ssrf-agent模块,使用的时候只要将其传入即可实现一次解析,多次判断的功能,下面是简单的使用示例:

    结束语

    SSRF可以说是经久不衰的漏洞攻击了,早些年百度、人人、360搜索等都有过相应的案例。一般以下场景可能会存在SSRF问题,我们需要多加注意:

    1.能够对外发起网络请求的地方,就可能存在SSRF漏洞。

    2.从远程服务器请求资源(Upload from URL,Import&Export RSS Feed)。

    3.数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)。

    4.Webmail收取其他邮箱邮件(POP3、IMAP、SMTP)。

    5.文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)。

    本文如未解决您的问题请添加抖音号:51dongshi(抖音搜索懂视),直接咨询即可。

    热门图文

    • 戏赠张叔甫黄庭坚的其他诗词有哪些

      《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。作者:黄庭坚。年代:宋代。我们为您从以下几个方面提供戏赠张叔甫的详细介绍。一、《戏赠张叔甫》的全文 点此查看《戏赠张叔甫》的详细内容。团扇复团扇,因风托方便。衔泥巢君屋,双燕令人羡。张公子,时相见。张公一生江海客,文章献纳麒麟殿。文采风流今尚存,看君不合长贫贱。醉中往往爱逃禅,解道澄江静如练。淮南百宗经行处,携手落日回高宴。城上乌,尾毕逋。尘沙立暝途,惟有摩尼珠。云梦泽南州,更有赤须胡。与君歌一曲,长铗归来乎。出无车,食无鱼。不须闻此意惨怆,幸是元无免破除。脱吾帽,向君笑。

    • 戏赠张叔甫黄庭坚的其他诗词

      《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。作者:黄庭坚。年代:宋代。我们为您从以下几个方面提供戏赠张叔甫的详细介绍。一、《戏赠张叔甫》的全文 点此查看《戏赠张叔甫》的详细内容。团扇复团扇,因风托方便。衔泥巢君屋,双燕令人羡。张公子,时相见。张公一生江海客,文章献纳麒麟殿。文采风流今尚存,看君不合长贫贱。醉中往往爱逃禅,解道澄江静如练。淮南百宗经行处,携手落日回高宴。城上乌,尾毕逋。尘沙立暝途,惟有摩尼珠。云梦泽南州,更有赤须胡。与君歌一曲,长铗归来乎。出无车,食无鱼。不须闻此意惨怆,幸是元无免破除。脱吾帽,向君笑。

    • 戏赠张叔甫黄庭坚其他诗词

      《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。《戏赠张叔甫》黄庭坚其他诗词:《秋怀二首》、《登快阁》、《诉衷情》、《虞美人宜州见梅作》、《寄黄几复》。作者:黄庭坚。年代:宋代。我们为您从以下几个方面提供戏赠张叔甫的详细介绍。一、《戏赠张叔甫》的全文 点此查看《戏赠张叔甫》的详细内容。团扇复团扇,因风托方便。衔泥巢君屋,双燕令人羡。张公子,时相见。张公一生江海客,文章献纳麒麟殿。文采风流今尚存,看君不合长贫贱。醉中往往爱逃禅,解道澄江静如练。淮南百宗经行处,携手落日回高宴。城上乌,尾毕逋。尘沙立暝途,惟有摩尼珠。云梦泽南州,更有赤须胡。与君歌一曲,长铗归来乎。出无车,食无鱼。不须闻此意惨怆,幸是元无免破除。脱吾帽,向君笑。

    • 紴字的组词 紴字的组词有哪些

      紴字的组词 紴字的组词有哪些。麻紴、;紴的笔顺是撇折、撇折、点、点、点、点、横撇。紴字的笔顺图解;共十一画;紴;1;撇折;2;撇折;3;点;4;点;5;点;6;点;7;横撇;8;撇;9;竖;10;横撇;11;捺;紴的拼音为bō,部首为糹,结构为左右,注音为ㄅㄛ,ㄅㄧˋ。紴字的具体字的具体解释是什么呢,我们通过以下几个方面为您介绍。一、基本解释 点此查看紴的详细内容。[ bō ]1.绦属。2.锦类。3.水波锦文。[ bì ]1.装束貌。二、康熙字典;紴【未集中】【糸部】 康熙筆画:11画,部外筆画:5画《廣韻》博禾切《集韻》逋禾切。音波。《說文》絛屬。又《集韻》匹靡切,音帔。《玉篇》水紴錦文也。又《集韻》平義切,音被。《類篇》裝束貌。三、说文解字;

    • 戏赠张叔甫相同朝代诗歌 戏赠张叔甫相同朝代诗歌有哪些

      《戏赠张叔甫》黄庭坚相同朝代诗词。《曹将军》、《下瞿塘》、《滕王阁》、《儿馁嗔郎罢妻寒怨藁砧唐眉山诗也戏为笺之》、《题艾溪》、《废贡院为米廪过之值盘厫》、《送客至灵谷》、《叹屩词》、《题陈朝玉爱竹轩》、《酬春湖史履庸惠四皓图》。《戏赠张叔甫》相同朝代的诗词:《曹将军》、《下瞿塘》、《滕王阁》、《儿馁嗔郎罢妻寒怨藁砧唐眉山诗也戏为笺之》、《题艾溪》、《废贡院为米廪过之值盘厫》、《送客至灵谷》、《叹屩词》、《题陈朝玉爱竹轩》、《酬春湖史履庸惠四皓图》作者:黄庭坚。年代:宋代。我们为您从以下几个方面提供戏赠张叔甫的详细介绍。一、《戏赠张叔甫》的全文 点此查看《戏赠张叔甫》的详细内容。团扇复团扇,因风托方便。衔泥巢君屋,双燕令人羡。张公子,时相见。张公一生江海客,文章献纳麒麟殿。

    Top