2023年2月

在我的混合式开发框架里面,集成了WebAPI的访问,这种访问方式不仅可以实现简便的数据交换,而且可以在多种平台上进行接入,如Winform程序、Web网站、移动端APP等多种接入方式,Web API的处理方式和微信提供的接口处理规则类似,也是通过向服务器获得访问令牌(AccessToken),然后传递给每个Web API接口,实现数据的交换处理。本篇随笔主要介绍混合框架中Winform对Web API访问的处理。

1、Web API接入方式介绍

《混合式开发框架》混合了Web API接口访问、WCF接口访问,以及直接访问数据库三种方式的接入,以适应多种场景的应用,是基于门面层的一种接口实现处理和封装。是一种弹性化非常好的框架应用,既可用于单机版软件或者基于局域网内的应用软件,也可以用于分布式技术的互联网环境应用,是一种成熟稳定、安全高效的技术框架。

关于这个框架的详细介绍,可以查看我的随笔《
Winform混合式开发框架的特点总结
》进行详细了解。

这里主要关注Web API的接入方式,我们知道,如果是一般的接口,如果公布在互联网上面,就会有很多接入的风险,因此需要对接口的调用进行检查校验,确保访问令牌有效,而且对数据发生修改的,还需要对数据的加密签名进行检查,才能保证我们的接口运行在较为安全的环境中。

混合框架调用Web API接口的详细过程,可以通过《
Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解
》、《
Web API应用架构在Winform混合框架中的应用(1)
》、《
Web API接口设计经验总结
》进行了解。

2、Web API的接口访问令牌的处理

由于我们需要对接口访问的身份进行核实,因此一般要求我们的接口都带有一个token参数,用来对用户身份进行识别,如下所示是Web API层的MVC控制器的接口定义。

[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

如果
我们在客户端需要调用这个接口,那么就需要传入这个token参数,也就是说这个token令牌需要在调用任何接口前获得,这样才能为我们后面的接口调用做好准备。

而这个token的产生是非常重要的,需要严格颁发,因此需要对获取这个token的方法的参数进行签名校验,如下面代码是WebAPI接口对产生token的处理。

        /// <summary>
        ///注册用户获取访问令牌接口/// </summary>
        /// <param name="username">用户登录名称</param>
        /// <param name="password">用户密码</param>
        /// <param name="signature">加密签名字符串</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        /// <param name="appid">应用接入ID</param>
[HttpGet]public TokenResult GetAccessToken(string username, string password, string signature, string timestamp, string nonce, string appid)

也就是需要传入用户名、密码、加密签名、时间戳、随机数、应用接入ID等信息,从而构建出来一个访问令牌,通过用户名、密码、加密签名校验等方式,可以实现对访问令牌(token)的严格颁发处理。

在客户端调用所有Web API接口前,我们需要先通过上面的Web API接口,获取到该用户的访问令牌,为了方便,我们可以在客户端封装一个函数,通过这个函数获取到对应的访问令牌,然后把它存储在缓存里面,方便各个模块的接口访问处理。

    /// <summary>
    ///用户获取令牌的辅助类/// </summary>
    public classAccessTokenHelper
{
private const string APPID = "APPID";//应用ID,由系统管理员分配 private const string APPSECRET = "APPSECRET";//应用秘钥,,由系统管理员分配 private const string DEFAULT_API_URL = "http://localhost:9001/api/Auth/GetAccessToken";//默认调试的Web API获取授权地址 /// <summary> ///设置签名参数。///由于Web API大多数的接口,都需要验证用户身份的访问令牌(accesstoken),因此用户在登陆的时候,需要使用这个步骤去获取令牌信息,然后在继续后续的接口操作。///该接口用到的应用ID、应用秘钥等参数,由系统管理员统一分配。/// </summary> public static bool GetAccessToken(string username, stringpassword)
{
bool result = false;//配置使用Web API模式,需要构建登陆token才能访问 AppConfig config = newAppConfig();string callerType = config.AppConfigGet("CallerType");string apiUrl = config.AppConfigGet("AuthApiUrl");
apiUrl
= string.IsNullOrEmpty(apiUrl) ?DEFAULT_API_URL : apiUrl;if (callerType.Equals("api", StringComparison.OrdinalIgnoreCase))
{
//使用API方式,需要在缓存里面设置特殊的信息 var url = apiUrl +SignatureHelper.GetSignatureUrl(APPID, APPSECRET);
url
+= string.Format("&username={0}&password={1}", username, password);

TokenResult tokenResult
= JsonHelper<TokenResult>.ConvertJson(url);
result
= !string.IsNullOrEmpty(tokenResult.access_token);if (tokenResult == null)
{
var message = "获取授权信息出错,请检查地址是否正确!";
MessageDxUtil.ShowError(message);
}
var SignatureInfo = newSignatureInfo()
{
appid
=APPID,
appsecret
=APPSECRET,
token
= (tokenResult != null) ? tokenResult.access_token : null};
Cache.Instance.Add(
"SignatureInfo", SignatureInfo);
}
returnresult;
}

有了这个辅助方法,我们可以在程序启动后,用户进行身份登录的时候,先调用这个方法来获取令牌。

                string ip =NetworkUtil.GetLocalIP();string macAddr =HardwareInfoHelper.GetMacAddress();string loginName = this.txtLoginName.Text.Trim();string password = this.txtPassword.Text;//如果无法获取访问令牌,则返回
                bool hasGotToken =AccessTokenHelper.GetAccessToken(loginName, password);if (!hasGotToken)
{
return;
}

刚才我提到了Web API层的MVC控制器的接口定义,默认后面一般都有一个token参数,如下代码所示

[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

而我们为了方便客户端调用,一般在客户端调用Web API的时候进行简化了一下,把token参数拿掉,它的值从缓存里面提取。如客户端调用的封装代码如下所示。

        public UserInfo GetUserByName(stringuserName)
{
var action = "GetUserByName";string url = GetTokenUrl(action) + string.Format("&userName={0}", userName);

UserInfo result
= JsonHelper<UserInfo>.ConvertJson(url);returnresult;
}

其中GetTokenUrl就是我们根据token和方法名称,构建一个连接字符串,函数实现如下所示。

        /// <summary>
        ///获取单纯包含token参数的连接/// </summary>
        /// <param name="action">控制器方法名称</param>
        /// <returns></returns>
        protected string GetTokenUrl(stringaction)
{
string url = "";if (this.SignatureInfo != null)
{
var append = string.Format("?token={0}", SignatureInfo.token);string baseUrl =GetBaseUrl();
url
= CombindUrl(baseUrl, action + append);//组合为完整的访问地址 }else{throw new ArgumentNullException("没有在缓存里面设置SignatureInfo签名信息");
}
returnurl;
}

这样最终我们可以获得类似下面的连接地址:

http://localhost:27206/api/Account/GetAccountTypeList?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNDYzNTU3OTAzLCJqdGkiOiI3OGMyOGRhNC01ZjRjLTQxYzItOThkNC1lYmFkZTM3YjA4NjUiLCJuYW1lIjoiYWRtaW4iLCJjaGFubmVsIjoiMCIsInNoYXJlZGtleSI6IjEyMzRhYmNkIn0.DysdbGx70xuIxXBz3G3x3MkGh9ZxL2zF9Fzu8FGVS0w

有了这个令牌组装好的URL,我们可以对访问结果的JSON字符串进行解析,把它解析为对应的数据就可以了。

当然,在实际的Web API接口开发过程中,我们还可以使用Web API工具进行接口调试,如下所示。

下面的1-5的标识就是获取token所需要的签名数据,当然连接还带有几个账号认证所需要的信息了,如账号密码、所在公司等信息。

当然我们也可以使用浏览器进行测试获取Token的信息,只是没有那么方便而已。

在很多时候,我们在数据库里面定义表字段和实际在页面中展示的内容,往往是不太匹配的,页面数据可能是多个表数据的综合体,因此除了我们在表设计的时候考虑周到外,还需要考虑数据展现的处理。如果是常规的处理,那么需要对部分外键字段进行特别的转义处理,如果需要增加多一些字段,那么这种处理可能就相对比较麻烦一些。本文介绍如何在MVC控制器里面使用dynamic和ExpandoObject,实现数据转义后一体化的输出,包括增加任意多的字段信息。

1、数据信息的展示

一般情况下,我们在界面里面展示的信息是相对比较丰富的,尽管我们设计数据表的时候,考虑的是如何精简且避免重复,但是在界面上展示的信息,往往是考虑如何让用户更加方便,因此可能尽可能的展示相关信息。

如对于这样的场景,设备信息作为主要的基础信息,其相关的业务包括设备检查、设备维护、设备报修等信息,如下所示。

基于上面的数据设计,我们如果在展示设备检查、设备维护、设备报修等信息的时候,那么我们一般还需要展示部分的设备基础信息,这样我们更容易了解整个记录数据,但是我们在数据设计的时候,是把它们分开的,因此需要在输出到界面的时候,把它们综合起来。

我以前在《
基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作
》介绍过一些数据转义的处理,不过那种方式并不是比较理想的方式。本篇介绍的使用dynamic和ExpandoObject才是我理想的处理模式。

我们来看看我最终通过这种方式实现的界面效果,之后我们再来一步步介绍如何实现这个操作过程的。

2、数据转义的实现

在上面的界面效果里面,我们是基于MVC实现后台的处理,在界面上利用Bootstrap进行展示的(利用EaysUI组件也是类似的处理)。我们分为两部分进行介绍实现的,一部分是采用MVC的输出数据,一部分是界面的展示。

1)MVC的控制器数据处理

在MVC里面,我们一般通过基类的FindWithPager进行数据的分页处理,基于如何在MVC控制器里面实现数据的分页处理,大家感兴趣可以参考《
基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用
》随笔进行了解。

常规的做法,如果是主表信息,我们可以把它们简单的输出,如下所示。

        public overrideActionResult FindWithPager()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);string where =GetPagerCondition();
PagerInfo pagerInfo
=GetPagerInfo();
List
<DeviceInfo> list = baseBLL.FindWithPager(where, pagerInfo);//Json格式的要求{total:22,rows:{}}//构造成Json的格式传递 var result = new { total = pagerInfo.RecordCount, rows =list };returnToJsonContentDate(result);
}

也就是不需要经过任何转义就直接把查询到的数据列表输出给调用者,由界面进行数据的筛选处理。

如果对于上面提到的设备检查、设备维修等和设备信息相关的,我们就需要利用dynamic和ExpandoObject,把设备信息整合一起提供给界面了,具体代码如下所示。

我们首先对查询的记录进行遍历,把每条记录进行转换,如下所示。

            List<ExpandoObject> objList = new List<ExpandoObject>();foreach (DeviceCheckInfo info inlist)
{
dynamic obj = new ExpandoObject();

注意上面我们定义了List<ExpandoObject>的列表和dynamic obj的对象,这样我们通过动态定义的对象,把我们需要的字段属性加到动态对象里面,然后放到集合里面即可。

完整的分页控制器代码如下所示。

        public overrideActionResult FindWithPager()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);string where =GetPagerCondition();
PagerInfo pagerInfo
=GetPagerInfo();
List
<DeviceCheckInfo> list = baseBLL.FindWithPager(where, pagerInfo);//设备编码 所属科室 品牌 品类 型号 设备序列号 检查时间 处理人 List<ExpandoObject> objList = new List<ExpandoObject>();foreach (DeviceCheckInfo info inlist)
{
dynamic obj = newExpandoObject();

DeviceInfo deviceInfo
= BLLFactory<Device>.Instance.FindByCode(info.DeviceCode);if (deviceInfo != null)
{
obj.Dept
=deviceInfo.Dept;
obj.Brand
=deviceInfo.Brand;
obj.Name
=deviceInfo.Name;
obj.Model
=deviceInfo.Model;
obj.SerialNo
=deviceInfo.SerialNo;
}
obj.ID
=info.ID;
obj.DeviceCode
=info.DeviceCode;
obj.OperateTime
=info.OperateTime;
obj.Operator
=info.Operator;

objList.Add(obj);
}
//Json格式的要求{total:22,rows:{}}//构造成Json的格式传递 var result = new { total = pagerInfo.RecordCount, rows =objList };returnToJsonContentDate(result);
}

2)界面的数据展示

上面定义了数据的获取方式,也就是我们需要任何数据都可以在MVC控制器里面,通过动态属性的方式添加到集合对象里面,从而简化了我们界面的处理,我们只需要把获得的信息展示在界面上即可,非常简便了。

界面视图的HTML代码如下所示

            <tableid="grid"class="table table-striped table-bordered table-hover"cellpadding="0"cellspacing="0"border="0"class="display"width="100%">
                <theadid="grid_head">
                    <tr>
                        <!--设备编码    所属科室    品牌    品类    型号    设备序列号    检查时间    处理人-->
                        <thclass="table-checkbox"style="width:40px"><inputclass="group-checkable"type="checkbox"onclick="selectAll(this)"></th>
                         <th>设备编码</th>
                         <th>所属科室</th>
                         <th>品牌</th>
                         <th>品类</th>
                         <th>型号</th>  
                        <th>设备序列号</th>
                        <th>检查时间</th>
                        <th>处理人</th>
                        <thstyle="width:90px">操作</th>
                    </tr>
                </thead>
                <tbodyid="grid_body"></tbody>
            </table>

我们绑定到界面上,是通过Ajax的方式获取数据,然后绑定显示的,JS代码如下所示。

        functionSearchCondition(page, condition) {//获取Json对象集合,并生成数据显示内容
            url = "/DeviceCheck/FindWithPager?page=" + page + "&rows=" +rows;
$.getJSON(url
+ "&" + condition, function(data) {
$(
"#totalCount").text(data.total);
$(
"#totalPageCount").text(Math.ceil(data.total /rows));

$(
"#grid_body").html("");//<!--设备编码 所属科室 品牌 品类 型号 设备序列号 检查时间 处理人 --> $.each(data.rows, function(i, item) {var tr = "<tr>";
tr
+= "<td><input class='checkboxes' type=\"checkbox\" name=\"checkbox\" value=" + item.ID + "></td>";
tr
+= "<td>" + item.DeviceCode + "</td>";
tr
+= "<td>" + item.Dept + "</td>";
tr
+= "<td>" + item.Brand + "</td>";
tr
+= "<td>" + item.Name + "</td>";
tr
+= "<td>" + item.Model + "</td>";
tr
+= "<td>" + item.SerialNo + "</td>";
tr
+= "<td>" + item.OperateTime + "</td>";
tr
+= "<td>" + item.Operator + "</td>";

tr
+= getActionHtml(item.ID); //获取查看、编辑、删除操作代码 tr+= "</tr>";
$(
"#grid_body").append(tr);
});
//设置分页属性及处理 var element = $('#grid_paging');if(data.total > 0) {var options ={
bootstrapMajorVersion:
3,
currentPage: page,
numberOfPages: rows,
totalPages: Math.ceil(data.total
/rows),
onPageChanged:
function(event, oldPage, newPage) {
SearchCondition(newPage, condition);
//页面变化时触发内容更新 }
}
element.bootstrapPaginator(options);
}
else{
element.html(
"");
}
});
}

这样就最终优雅的实现了我们前面介绍的界面效果了。


在现今很多网站里面,都使用了微信开放平台的扫码登录认证处理,这样做相当于把身份认证交给较为权威的第三方进行认证,在应用网站里面可以不需要存储用户的密码了。本篇介绍如何基于微信开放平台的扫码进行网站的登陆处理。

1、开放平台的认证

要使用网站的扫码登录处理,就需要先进行微信开放平台帐号的开发者资质认证,提交相关的资料,以及交付每年300元的认证费用。

认证后,建立相关的网站应用后,就有相关的APPID和APPSecret了,这些关键的参数就可以用来获取相关的用户信息了。

网站应用的应用详情界面如下所示。

整个开放平台感觉没有多少东西,不过需要收费认证才能使用这些功能,感觉不是很爽。

我们采用的扫码登录,需要通过开放平台获取用户的信息,因此还需要设置获取用户基本信息接口的域名,否则无法获取信息,从而会导致重定向出错。

设置域名在【接口权限】【网页账号】【网页授权获取用户基本信息】的修改入口,如下图所示。

然后在弹出的对话框里面输入授权回调的域名即可。

这样设置就可以确保获取到用户信息了。

2、扫码登录的说明和具体使用

网站应用微信登录是基于
OAuth2.0协议标准
构建的微信OAuth2.0授权登录系统。

在进行微信OAuth2.在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。

微信OAuth2.0授权登录让微信用户使用微信身份安全登录第三方应用或网站,在微信用户授权登录已接入微信OAuth2.0的第三方应用后,第三方可以获取到用户的接口调用凭证(access_token),通过access_token可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。

微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有server端的应用授权。该模式整体流程为:

1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

获取access_token时序图:

从上图我们可以大概了解整个扫码登陆的处理过程。

3、扫码登录的各个步骤处理

1)用户身份的绑定

为了实现二维码扫码登录,我们需要在现有系统里面绑定用户的微信,这样才能在用户扫码的时候,判断用户的身份从而实现自动登录的过程。

我们可以在用户管理里面进行统一设置,也可以在常规用户登录(用户名+密码)后进行设置,这个主要看我们是否需要保留用户名密码登陆这种方式。

例如可以在用户管理里面统一绑定,也就是在创建用户的时候,让用户绑定下微信,获取微信的唯一标识。

也可以在保留用户名密码的登陆方式外,让用户登陆系统后自行进行绑定微信即可。

上面的界面,就是在一个页面里面弹出一个层,然后请求二维码显示即可,如下界面代码所示。

        <divid="divWechat"class="easyui-dialog"style="width:450px;height:350px;padding:10px 20px"closed="true"resizable="true"modal="true"iconcls="icon-setting">
            <div>
                <h4>扫描用户二维码,进行绑定</h4>
            </div>
            <divalign="center">
                <imgid="imgQRcode"alt="使用微信扫码进行绑定"style="height:200px;width:auto" />
            </div>

            <divalign="right">
                <ahref="javascript:void(0)"class="easyui-linkbutton"iconcls="icon-cancel"onclick="javascript: $('#divWechat').dialog('close')">关闭</a>
            </div>
        </div>

上面的层在打开的时候,我们使用JS来动态获取二维码进行显示,具体JS代码如下所示。

    //绑定微信登陆
    functionBindWechat() {var url = "http://www.iqidi.com/H5/BindWechat?id=@Session["UserID"]";
url
=encodeURIComponent(url);
$(
"#imgQRcode").attr("src", "/H5/QR?url=" + url);//打开绑定窗口 $("#divWechat").dialog('open').dialog('setTitle', '使用微信扫码进行绑定');
}

上面的JS只是做前端的数据请求和显示,具体的QR动作Action其实就是生成扫描二维码的过程,这个二维码其实就是采用通用的方式,来构建一个指向我们绑定账号的地址,从而实现我们绑定账号的判断,二维码的生成过程如下所示。

        /// <summary>
        ///转换二维码连接为图片格式/// </summary>
        /// <param name="url">二维码连接</param>
        /// <returns></returns>
[HttpGet]public ActionResult QR(stringurl)
{
//初始化二维码生成工具 QRCodeEncoder qrCodeEncoder = newQRCodeEncoder();
qrCodeEncoder.QRCodeEncodeMode
=QRCodeEncoder.ENCODE_MODE.BYTE;
qrCodeEncoder.QRCodeErrorCorrect
=QRCodeEncoder.ERROR_CORRECTION.M;
qrCodeEncoder.QRCodeVersion
= 0;
qrCodeEncoder.QRCodeScale
= 4;//将字符串生成二维码图片 var image =qrCodeEncoder.Encode(url, Encoding.Default);//保存为PNG到内存流 MemoryStream ms = newMemoryStream();
image.Save(ms, ImageFormat.Png);
image.Dispose();
return File(ms.ToArray(), "image/Png");
}

为了实现用户的绑定,我们需要获取当前用户的身份信息,因此需要在BindWeChat的操作里面做一个转向处理,如下接口所示。

        /// <summary>
        ///生成绑定微信的地址/// </summary>
        /// <returns></returns>
        public ActionResult BindWechat()

这个函数处理里面,我们需要重新定向处理,我们把它定向到BindAccount函数里面,方便获取用户的openid和其他必要的信息。

另外我们基于微信开放平台的应用,建立了一个和微信账号信息的联系,因此创建数据库信息如下所示。

也就是一个具体的开放平台应用对应着一个具体的微信账号,这样我们就可以充分利用配置进行处理了。

上面提到的BindAccount的处理的逻辑就是获取必要的信息,然后在数据库层面对身份信息进行验证,具体代码如下所示。

        /// <summary>
        ///绑定用户微信号/// </summary>
        /// <param name="id">账号ID</param>
        /// <returns></returns>
        publicActionResult BindAccount()
{
WebAppInfo appInfo
=GetWebApp(ConfigData.WebAppId);
AccountInfo accountInfo
=GetAccount(appInfo.AccountNo);var htResult = GetOpenIdAndUnionId(accountInfo.UniteAppId, accountInfo.UniteAppSecret);//存储openid方便使用 string openid = htResult["openid"].ToString();var unionid = htResult["unionid"].ToString();var userid = Request.QueryString["id"];var state = Request.QueryString["state"]; if (!string.IsNullOrEmpty(openid) && !string.IsNullOrEmpty(userid))
{
CommonResult result
= BLLFactory<User>.Instance.BindUser(openid, unionid, userid.ToInt32());if(result.Success)
{
returnBindSuccess();
}
else{returnBindFail();
}
}
else{throw new WeixinException("无法获取openid" + string.Format(", openid:{0}, userid:{1}", openid, userid));
}
}

在绑定的过程,我们需要考虑绑定正确账号,重复绑定其他账号,无效绑定几种情况,如果成功绑定正确账号(可多次处理结果一样),那么得到界面如下所示(这个界面的样式采用了weui的样式)。


2)用户的扫码登录处理

上面绑定了账号后,就可以通过扫码进行登录了,扫码回调的时候我们有自己的判断处理,扫码界面如下所示(我们在保留用户名密码登陆的方式外,增加了一个扫码登录的处理)。

如果是Bootstrap的界面效果

如果是EasyUI的界面效果

这个和前面的二维码显示规则差不多,不过他们的连接地址是不同的,这个地方用到了开放平台的接口,也就是我们前面提到开放平台认证的接口了。

上面的扫码登录的界面代码如下所示。

    <!--二维码扫描登陆的界面层-->
    <divid="divWechat"class="easyui-dialog"style="width:550px;height:500px;padding:10px 20px"closed="true"resizable="true"modal="true"iconcls="icon-setting">
        <divid="login_container"align="center">
        </div>

        <divalign="right">
            <ahref="javascript:void(0)"class="easyui-linkbutton"iconcls="icon-cancel"onclick="javascript: $('#divWechat').dialog('close')">关闭</a>
        </div>
    </div>

上面代码需要引入JS文件,并使用微信JSSDK的API进行显示的。

    <!--使用微信扫码进行登陆-->
    <scriptsrc="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
    <scriptlanguage="javascript">

        functionOpenJSLogin() {varobj= newWxLogin({
id:
"login_container",
appid:
"@ViewBag.appid",
scope:
"snsapi_login",
redirect_uri:
"@ViewBag.redirect_uri",
state:
"@ViewBag.state",
style:
"black",
href:
".impowerBox .qrcode {width: 200px;}"});//打开绑定窗口 $("#divWechat").dialog('open').dialog('setTitle','使用微信扫码进行登陆');
}
</script>

这个里面的参数,如APPID就是来源我们认证后的开放平台参数。

这些信息我们在MVC控制器后面获取后绑定在ViewBag,方便界面前端的使用。

            //使用JSLogin登陆
            WebAppInfo appInfo = BLLFactory<WebApp>.Instance.FindByID(ConfigData.WebAppId);
ArgumentValidation.CheckForNullReference(appInfo,
"Web应用程序appInfo");if (appInfo != null)
{
ViewBag.appid
=appInfo.OpenAppID;
ViewBag.redirect_uri
=appInfo.LoginCallBackUrl;
ViewBag.state
=ConfigData.AuthState;
}

其中的redirect_uri是通过数据库获取的LoginCallBackUrl地址,这个地址类似如下格式:
http://www.iqidi.com/H5/callback?uid=iqidiSoftware

也就是我们在开放平台处理返回后进行的回调处理。

通过开放平台的APPID和APPSecret,我们可以获取到对应的接口调用凭证,然后根据接口凭证,以及openid,获得用户的公众平台统一的UnionID,这个标识是我们用户的唯一标识,代码如下所示。

                var result =baseApi.GetAuthToken(appid, appsecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
openid
=result.openid;var unionResult =baseApi.GetSnsapiUserInfo(result.access_token, result.openid);

ht.Add(
"openid", openid);
ht.Add(
"unionid", unionResult != null ? unionResult.unionid : "");
}

有了unionid我们就可以根据这个标识在我们的用户数据库里面查找对应的用户,如下代码所示。

            //开放平台的OpenID,不是公众号的OpenID,需要转换为unionid
            if (!string.IsNullOrEmpty(openid) && !string.IsNullOrEmpty(unionid))
{
UserInfo userInfo
= BLLFactory<User>.Instance.FindByUnionId(unionid);

然后判断我们去到的用户信息是否正确,如下代码所示

                if (userInfo != null)
{
CommonResult loginResult
=CheckLogin(userInfo.Name);if (!loginResult.Success)
{
LogHelper.Info(
string.Format("用户登陆不成功,{0}", loginResult.ErrorMessage));
}
//登陆成功后的重定向地址 var url = appInfo.HomeUrl; //例如:http://www.iqidi.com/Home returnRedirect(url);
}

如果不成功,那么我们定向到指定的界面即可。

            //如不成功,最后都统一提示信息
            ViewBag.Error = "获取信息失败,登陆错误";return View("LoginError");

如果我们登陆成功后,需要设置一些Session信息或者Cookie信息,那么就可以通过CheckLogin函数进行处理即可。

以上就是我们结合微信开放平台实现微信扫码登录的过程,其中整个过程就是用到了下面几个过程。

1)使用JSSDK的脚本实现扫码获取code

JS微信登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。 网站内嵌二维码微信登录JS实现办法:

步骤1:在页面中先引入如下JS文件(支持https):

<scriptsrc="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>

步骤2:在需要使用微信登录的地方实例以下JS对象:

                          var obj = newWxLogin({
id:
"login_container",
appid:
"",
scope:
"",
redirect_uri:
"",
state:
"",
style:
"",
href:
""});

2) 第二步:通过code获取access_token

通过code获取access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

3)第三步:通过access_token调用接口

获取access_token后,进行接口调用,

对于接口作用域(scope),能调用的接口有以下:

授权作用域(scope) 接口 接口说明
snsapi_base /sns/oauth2/access_token 通过code换取access_token、refresh_token和已授权scope
/sns/oauth2/refresh_token 刷新或续期access_token使用
/sns/auth 检查access_token有效性
snsapi_userinfo /sns/userinfo 获取用户个人信息

其中snsapi_base属于基础接口,若应用已拥有其它scope权限,则默认拥有snsapi_base的权限。使用snsapi_base可以让移动端网页授权绕过跳转授权登录页请求用户授权的动作,直接跳转第三方网页带上授权临时票据(code),但会使得用户已授权作用域(scope)仅为snsapi_base,从而导致无法获取到需要用户授权才允许获得的数据和基础功能。

4)获取信息在回调界面中进行登录前处理

通过上面接口,我们可以获得相应的用户身份信息,因此可以结合我们用户数据库进行用户身份的认定和处理,并设置必要的Session或者Cookie信息等,最后定位到我们的应用主界面即可。

在很多情况下,我们利用IOC控制反转可以很方便实现一些接口的适配处理,可以在需要的时候切换不同的接口实现,使用这种方式在调用的时候,只需要知道相应的接口接口,具体调用哪个实现类,可以在配置文件中动态指定,本篇主要介绍AutoFac的IOC组件的使用,用来实现微信接口处理的控制反转功能。

我们知道,实
现IOC的方式有很多,如Unity、AutoFac、Ninject、Castle Windsor、Spring.NET等等,每种IOC组件均有自己的一些特点,我在之前的实体框架随笔系列介绍过Unity的使用《
Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)
》,本来也想用这个来实现微信的接口调用处理,不过由于其版本以及一些其他问题,总是没有那么方便,最后决定使用也比较流行,应用较多的的AutoFac组件来实现。

1、微信接口的处理需求

我们在使用微信公众号实现一些业务处理的时候,往往需要根据不同的条件进行不同的接口调用。

如通过二维码扫码的结果处理,然后呈现给微信用户的相关信息,有下面两种方式。

根据用户的扫码结果,我们可以自定义自己的业务处理,然后呈现给用户,那么这里使用IOC来实现具体的业务是比较好的,我们在具体的业务实现里面,可以根据不同的条件实现所需要的复杂处理。

当然我们还可以扩展到很多的业务接口里面,如百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等,还有短信、邮件发送等常规接口,都可以使用这种方式进行处理。

接口的效果展示如下所示。

这些给其他项目模块使用的时候,我们可以在配置文件里面指定具体的接口实现信息,这种可以具体指定所需的实现。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionname="autofac"type="Autofac.Configuration.SectionHandler, Autofac.Configuration"/>
  </configSections>
  <autofacdefaultAssembly="WHC.Common.Handler">
    <components>
      <componenttype="WHC.Common.Handler.TestHandler, WHC.Common.Handler"service="WHC.Common.Handler.ITestHandler" />
      <componenttype="WHC.Common.Handler.QRCodeHandler, WHC.Common.Handler"service="WHC.Common.Handler.IQRCodeHandler" />
      <!--邮件短信-->
      <componenttype="WHC.Common.Handler.SmsSendHandler, WHC.Common.Handler"service="WHC.Common.Handler.ISmsHandler" />
      <componenttype="WHC.Common.Handler.MailSendHandler, WHC.Common.Handler"service="WHC.Common.Handler.IMailHandler" />
      
    </components>
  </autofac>
</configuration>

直接使用AutoFac的操作应该是比较方便,使用接口获取方式获取具体实现就可以了。

2、使用Autofac实现

为了方便使用Autofac,我们可以先在项目上的Nuget包管理,引用相关的DLL,其中包括核心的Autofac类库,以及读取配置文件的Autofac Configuration,后者为方便读取XML配置信息所必须。

引入这两个DLL就可以使用Autofac的功能了。

一般通过配置文件,初始化的Autofac组件的代码如下所示

    instance = newAutoFactory();//初始化相关的注册接口
    var builder = newContainerBuilder();//从配置文件注册相关的接口处理
    builder.RegisterModule(new ConfigurationSettingsReader("autofac", configurationFile));
container
= builder.Build();

而我们使用Autofac的接口也是很容易的,常规的使用代码如下所示。

            var handler = container.Resolve<ITestHandler>();
handler.Test(
"测试");

当然,为了方便,我们可以使用一个辅助类来简化这个接口的调用:在辅助类初始化的时候,我们从配置文件加载对应的组件接口实现,当我们需要解析具体接口的时候,就可以直接从Container容器里面胡获取了,辅助类代码如下所示。

    /// <summary>
    ///使用AutoFac的工厂类,通过配置/// </summary>
    public classAutoFactory
{
//普通局部变量 private static object syncRoot = newObject();//工厂类的单例 private static AutoFactory instance = null;//配置文件 private const string configurationFile = "autofac.config";/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IContainer Container { get; set; }/// <summary> ///IOC容器工厂类的单例/// </summary> public staticAutoFactory Instatnce
{
get{if (instance == null)
{
lock(syncRoot)
{
if (instance == null)
{
instance
= newAutoFactory();//初始化相关的注册接口 var builder = newContainerBuilder();//从配置文件注册相关的接口处理 builder.RegisterModule(new ConfigurationSettingsReader("autofac", configurationFile));
instance.Container
=builder.Build();
}
}
}
returninstance;
}
}
/// <summary> ///测试的接口/// </summary> public voidTest()
{
var handler = AutoFactory.Instatnce.Container.Resolve<ITestHandler>();
handler.Test(
"测试");
}
}

3、外部接口实现及调用

这样我们所有的接口都定义好,并给每个定义的接口相应个实现就可以使用这个Autofac组件进行调用了。

    /// <summary>
    ///短信发送接口/// </summary>
    public interfaceISmsHandler
{
/// <summary> ///发送短信/// </summary> /// <param name="content">短信内容</param> /// <param name="mobiles">手机号码(多个号码用”,”分隔)</param> /// <param name="sendTime">预约发送时间</param> /// <returns></returns> CommonResult Send(string content, string mobiles, DateTime? sendTime = null);/// <summary> ///查询剩余条数/// </summary> /// <returns></returns> CommonResult GetLeftCount();
}
    /// <summary>
    ///邮件发送接口/// </summary>
    public interfaceIMailHandler
{
/// <summary> ///发送外部邮件(自定义邮件配置,如个人邮件)/// </summary> /// <param name="mailInfo">发送邮件信息</param> /// <param name="settingInfo">SMTP协议设置信息</param> /// <returns></returns> CommonResult Send(MailInfo mailInfo, SmtpSettingInfo settingInfo);/// <summary> ///发送外部邮件(系统配置,系统邮件)/// </summary> /// <param name="mailInfo">发送邮件信息</param> /// <returns></returns> CommonResult Send(MailInfo mailInfo);
}

例如,测试发送短信和邮件的IOC调用代码如下所示

            //使用IOC模块发送
            var sms = AutoFactory.Instatnce.Container.Resolve<ISmsHandler>();var smsTemplate = string.Format("验证码:{0}。尊敬的会员,您好,您正在注册会员,验证码2分钟内有效,感谢您的支持。", new Random().Next(100000));var result = sms.Send(smsTemplate, "18620292076");
Console.WriteLine(result.Success
? "发送短信成功" : "发送短信失败:" +result.ErrorMessage);

MailInfo info
= newMailInfo();
info.ToEmail
= "wuhuacong@163.com";
info.FromEmail
= "wuhuacong@163.com";
info.Subject
= "这是一份来自我自己的测试邮件";
info.Body
= info.Subject + ",这是内容部分。<a href='http://www.iqidi.com'>点击这里返回主页</a>";var mail = AutoFactory.Instatnce.Container.Resolve<IMailHandler>();var mailResult =mail.Send(info);
Console.WriteLine(mailResult.Success
? "发送邮件成功" : "发送邮件失败:" + mailResult.ErrorMessage);

测试后得到的结果如下:

邮件结果一样可以收到。

我们回到上面介绍的二维码扫描的业务实现效果,上面提到了,一个二维码事件可以派生出不同的接口实现,从而给不同的响应信息。

    /// <summary>
    ///扫码进行的处理/// </summary>
    public interfaceIQRCodeHandler
{
/// <summary> ///处理ScancodePush的事件/// </summary> /// <param name="info">扫描信息</param> /// <param name="accountInfo">账号信息</param> /// <returns></returns> stringHandleScancodePush(RequestEventScancodePush info, AccountInfo accountInfo);/// <summary> ///处理ScancodeWaitmsg的事件/// </summary> /// <param name="info">扫描信息</param> /// <param name="accountInfo">账号信息</param> /// <returns></returns> stringHandleScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo);
}

我们可以定义两个简单的接口处理,用来承接微信二维码扫描接口的处理操作。

这样我们在处理二维码扫描事件的时候,我们就可以把它分配到接口里面进行处理即可。

        /// <summary>
        ///扫码推事件的事件推送处理/// </summary>
        /// <param name="info">扫描信息</param>
        /// <returns></returns>
        public stringHandleEventScancodePush(RequestEventScancodePush info, AccountInfo accountInfo)
{
string result = "";var handler = AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>();if(handler != null)
{
result
=handler.HandleScancodePush(info, accountInfo);
}
returnresult;
}
/// <summary> ///扫码推事件且弹出“消息接收中”提示框的事件推送的处理/// </summary> /// <param name="info">扫描信息</param> /// <returns></returns> public stringHandleEventScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo)
{
string result = "";try{var handler = AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>();if (handler != null)
{
result
=handler.HandleScancodeWaitmsg(info, accountInfo);
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
}
returnresult;
}

对于其中之一的接口处理,我们都可以把它分拆,根据扫描的事件键值Key进行不同的信息相应。

        /// <summary>
        ///扫描后,会等待事件处理结果返回给用户/// </summary>
        public stringHandleScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo)
{
ResponseText response
= newResponseText(info);
response.Content
= string.Format("您的信息为:{0},可以结合后台进行数据查询。", info.ScanCodeInfo.ScanResult);var result =response.ToXml();string devicecode = GetParam(info.ScanCodeInfo, "devicecode");//参数名为小写 if (!string.IsNullOrEmpty(devicecode))
{
switch(info.EventKey.ToLower())
{
case "device_view"://设备查看 {var deviceinfo = BLLFactory<Device>.Instance.FindByCode(devicecode);
response.Content
=ConvertDeviceInfo(deviceinfo);
result
=response.ToXml();
}
break;case "measure"://设备计量 {var deviceinfo = BLLFactory<Device>.Instance.FindByCode(devicecode);
response.Content
=ConvertMeasure(deviceinfo);
result
=response.ToXml();
}
break;case "repair"://设备报修,返回报修单号 {var content =ConvertRepaire(info, accountInfo, devicecode);
response.Content
=content;
result
=response.ToXml();
}
break;case "inventory"://设备盘点,转到盘点界面 {var content =ConvertInventory(info, accountInfo, devicecode);
response.Content
=content;
result
=response.ToXml();
}
break;case "maintain":break;case "check":break;case "device_add":break;
}
}
returnresult;
}

以上就是关于使用Autofac实现一些常规接口处理的实现,这种控制反转的方式,可以便于我们项目的开发效率,可以根据需要指定一些特定的实现处理即可,而且通过配置文件的方式加载,可以很方便的进行配置。

我们在开发微信相关的应用的时候,一般需要完善的基础模块支持,包括微信公众号,微信企业号,以及一些业务模块的支持,一般随着功能的增多,我们需要非常清晰的界定他们的关系。模块的分拆以及合并往往需要考虑的代码的重用,而且尽量做到简单而不重复。本篇随笔基于我的微信框架的各个模块的功能介绍以及他们关系的描述。

1、公众号模块的命名及相关关系

微信开发,我们首先需要利用我们的语言(这里是利用C#语言),为所有用到的API接口实现进一步的封装,方便使用,微信API模块包含的内容很多,大概可以分为下面的项目。

有了这些接口功能的封装类,只是万里长征的第一步,我们还需要围绕这些接口,以及我们的业务模块实现更多交互功能的。

我们在WHC.Weixin.Data模块里面,定义了包含公众号的消息分派处理接口,这个分派接口是对接收来自微信服务器的各种消息事件进行响应;另外该模块还包含一些常规的数据存储,如关注用户、菜单、文章内容等方面数据的存储,如下所示。

当然,这个WHC.Weixin.Data是集大成者,它需要使用WHC.Weixin.API的项目内容来做数据提交,同时也是需要使用内部的数据存储处理模块。

2、企业号模块的命名及相关关系

企业号的做法和公众号类似,也是需要对微信提供的各种API进行封装,方便我们后面的接口调用,不过企业号目前支持的功能相对公众号少一些,大概包括有基础接口、企业号应用接口、菜单管理、通讯录管理、消息管理、摇一摇周边等模块。随着企业号功能的逐步完善和加入,可能腾讯会加入更多的一些功能模块。

同样我们参考微信公众号的做法,也是建立一个数据存储管理的项目,作为微信消息事件的处理入口,同时也管理存储一些必须的数据,包括需要同步的用户、标签、部门等数据。

随着微信公众号和企业号的功能逐渐统一,很多接口的交互数据几乎是一样的,因此我们可以把公用的实体类部分作为一个独立的项目,方便公众号和企业号两个项目的共同使用,这个项目命名为
WHC.Common.Entity

它们几个项目关系如下所示。

项目目录如下所示,包括了基础模块、摇一摇红包、菜单及多媒体管理模块、消息请求模块、消息应答模块、摇一摇周边、微信支付等。

由于微信支付的接口实现,是在企业号和公众号相对比较独立的一个API接口群,因此我们可以微信支付部分独立作为一个接口实现来处理,公众号或者企业号需要的时候,包含进去使用即可。

我们把它命名为WHC.Common.API项目。

还有我在前面随笔《
C#开发微信门户及应用(42)--使用Autofac实现微信接口处理的控制反转处理
》里面介绍过的基于对外部接口和二维码扫码处理接口的封装项目,命名为
WHC.Common.Handler

整个插件业务接口包括:百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等,还有短信、邮件发送等常规接口,都可以使用这种方式进行处理。接口的效果展示如下所示。

因此上面这些以WHC.Common命名的项目,基本上就是可以通用在公众号和企业号两边的项目模块了,它们包含前面介绍过的几个模块,如下所示。

当然,除了这些之外,我们做项目,一般还涉及到一些基础功能模块,如公用类库,以及附件管理、通讯录管理、权限管理模块等内容,我们可以把后者几个模块放在一起,组成基础模块。

3、微信界面项目

微信界面部分是前面模块组件的综合使用,在微信应用里面,一般需要使用80端口和微信服务器做交互,而这个同时往往也是我们项目的端口地址。

如果考虑的更好一些,我们可以采用
以Web API优先的理念来设计整个企业应用体系的。Web API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。

这样我们不管是Web项目、Winform项目,还是移动项目,都可以通过共同的接口Web API进行接入,实现更多元化的后台管理或者是前端界面呈现了。

Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,即使为了利用各自应用的特殊性,也可以把这些应用做的很相似,这样就给用户提供了一个统一的界面表示方式,极大提高客户使用的界面体验效果,用户几乎不需要额外的界面学习,就可以熟悉整个应用体系的各个模块使用。

这样最终就回到了我前面随笔介绍过的《
Web API应用架构设计分析(1)
》、《
Web API应用架构设计分析(2)
》大平台方案了,目前我正在努力整合所有微信的接口和相关的应用在一个大的Web API平台上,逐渐整合我目前应用较为广泛的混合式开发框架的相关技术和理念。

这样全面构建的Web API作为核心层,可以在上面开发我们各种企业业务应用,实现我们一个大平台的整合和多元化的应用,甚至我们可以把部分模块外包给更加擅长的团队,我们只需要确保我们核心的Web API层安全、健壮且具有良好的扩展性即可。