精华内容
下载资源
问答
  • 原创:飞书互动来源:飞书互动作为数字营销...付款前的每一个环节都有可能导致客户离开,环节越多,客户越有可能在未完成购买的情况下退出购物车。根据 BigCommerce 的 2019 年全渠道零售报告,便利性是美国各年龄...

    原创:飞书互动

    来源:飞书互动

    作为数字营销人员,在关心提高网站流量的同时,也不能忽视转化率的提升。本文为你介绍 7 条行之有效的方法,帮助你提供更流畅的用户体验,从而提升电商转化。

    1、简化付款流程

    “方便”是核心关键词。付款前的每一个环节都有可能导致客户离开,环节越多,客户越有可能在未完成购买的情况下退出购物车。

    根据 BigCommerce 的 2019 年全渠道零售报告,便利性是美国各年龄段消费者选择在线购物的三大原因之一。

    79e887904b1e13752fafb4d07317312c.png

    因此,你网站的付款流程越简单越好。比如,许多用户不愿意花时间注册账户,那么你就应该取消强制注册并提供“访客结账”的选项。

    如果某个环节确实无法取消,那就找到简化流程的方法。比如设置一个复制按钮,允许用户将其送货地址设为帐单邮寄地址,而无需重复输入相同的信息。

    ►可用工具:

    Convert.com:可以对你网站的购物体验进行 A/B Test,从而为客户提供最佳解决方案。

    05882aa3855d6812712ec712b832e248.png

    2、提供一键加售

    数据表明,开发新客户的成本是重新转化现有客户的 25 倍,如果可以将客户保留率提高 5%,就能将利润提高 25%。

    这意味着相比说服一个新客户购买你的产品,更有价值的方式是想方设法让现有客户追加购买——比如购买比原计划更大、更贵的商品,或者再多买一个商品。

    你可以通过一键式追加销售选项引导客户完成计划外购物,从而留住这些消费者。这正是冲动购物在实体店中的运作方式——将诱人的商品放在收银台旁边,让客户在排队结账时冲动购物。

    ►可用工具:

    PayKickstart:为供应商和数字出版商提供在线销售产品。

    571b0df398c82fb3cecb3d6e88f06795.png

    3、让购物体验适合移动设备

    移动购物正在不断增长。越来越多的人使用移动设备或平板电脑在电子商务网站上购物,随着全球智能手机的快速普及,这一数字将继续上升。

    如果难以在智能手机上浏览,用户更有可能放弃购物并离开网站。因此,你需要使你的网站在智能手机上易于阅读和使用。

    在不花钱开发独立应用程序的情况下,让移动端购物体验更好的一种强大方法是:使用支持渐进式网络应用程序(PWA)的网页设计平台。

    PWA 还支持大多数移动优化网站所没有的类似应用程序的功能,例如推送通知,这对于全渠道零售商尤其有用。

    ►可用工具:

    Duda:只需点击下图中示意的按钮,即可推出客户网站的渐进式网络应用程序版本。

    337e5722bfa311dd5b8e6b2950e0b523.png

    4、提供个性化的购物体验

    研究表明,个性化体验作为日益增长的电子商务趋势不容忽视:

    59% 的电子商务购物者发现,在个性化网站上购物更容易、更有吸引力。

    56% 的购物者越来越有可能返回向他们推荐产品的网站。

    53% 的购物者认为,提供个性化购物的电子商务零售商提供了有价值的服务。

    ►可用工具:

    1)Alter:帮助建立个性化的购物体验,工作原理如下:

    访客阅读内容或研究产品

    Alter 根据他们正在查看的网页(例如鞋子、汽车),匿名确定非个人访客的兴趣

    访问者可以根据自己的兴趣来浏览个性化内容,以帮助他们在以后访问的营销网站上节省时间

    83179ebb2fc7b136b246ade943f49471.png

    2)Duda

    内容管理系统,提供可靠的、更易实现的个性化选项,允许你根据一天中的时间、地理位置、访问次数等来设置个性化的 CTA 和特别优惠。

    8a0740d36714df5faf5a8a8bb38ccdd7.png

    5、满足客户的期望

    许多客户都会通过 Google 搜索发现产品,那么你的网站的登录页面是否能够满足他们的预期?

    搜索意图常常决定了用户的页面参与度,他们能否马上看到希望看到的东西,决定了他们是否愿意留下来,让你有机会把他们转化为买家。

    ►可用工具

    Text Optimizer:使用语义分析,从 Google 的搜索结果中提取重要概念,有针对性地进行登陆页面优化,吸引更多网站访问者。

    fbadd33150f4ec74f78b7fe6c28ffd18.png

    6、增加一种紧迫感

    “害怕错过”是一种强大的心理力量,你可以利用它,在购物车页面制造紧迫感,让客户产生 FOMO(错失恐惧症)心理,这可以有效推动犹豫的顾客,迫使他们完成购买。

    比如亚马逊使用了倒计时,告诉你在多长时间内购买该商品可以享受当日达。

    88bf74d0e11b2dda36e0b2f0d0c48ae9.png

    你可以在购物车页面添加计时器,或者销售结束的倒计时,甚至显示“立即结账”而不仅仅是“结账”,来制造紧张感。

    7、添加面包屑导航

    面包屑导航(Breadcrumbs)是一种基于网站层次信息的显示方式。以博客为例,面包屑导航可以显示发布日期、类别或标签。它们表示当前页面在导航层次结构内的位置。

    对电子商务网站来说,为 SEO 和可用性实现清晰的站点导航是必须的。通过设置面包屑导航,可以指引用户来到这个地方的路径,以及提示用户下一步的方向,有助于转化率上升。

    注意事项:

    . 显示层级结构,让面包屑导航做到精细化

    使用指向性的符号,便于用户查看

    避免分类信息的重复

    面包屑中添加关键字

    本文由 jqyjr 编辑排版

    推荐阅读

    从流量架构搭建到实现高ROI投放 品牌出海早期获客要做对这些事

    Facebook&白鲸出海《印度市场入门策略白皮书》解读(附下载链接)

    全球No.4博彩游戏公司SG提交上市申请:来看看美国这个比国内付费高十倍的博彩市场

    2019中国跨境电商出口趋势与机遇白皮书发布:供应链升级是竞争点 品牌全球化是趋势

    Flipkart剥离移动钱包 PhonePe业务将独立运作 欲融资10亿美元与Paytm等支付巨头竞争

    4073809c7cd1ee88295a303595fd2fdc.gif

    展开全文
  • 知识要点 1.实现原理: 通过递归改变div的高度值达到缓慢下拉的效果。 2.一共分为3个步骤我写了三个函数 第一个show()函数(下拉):初始值高度h<300的话 h+5 反之return退出停止,...完整代码 <!DOCTYPE ht
  • 影片详情开发也是通过Cheerio抓取并分析网页获取到的详情数据,本节就不作为详细内容来讲解了,详细的代码可以看下我的github,效果如下:在点击播放时,会跳转到播放界面,并且横屏显示,退出播放界面时,会恢复到...

    影片详情开发也是通过Cheerio抓取并分析网页获取到的详情数据,本节就不作为详细内容来讲解了,详细的代码可以看下我的github,效果如下:

    详情

    在点击播放时,会跳转到播放界面,并且横屏显示,退出播放界面时,会恢复到竖屏状态。但是,react native并没有给我们提供设置横竖屏的API,因此,我们需要自己使用原生的代码来完成此功能。

    使用原生代码,我们可以为react native作什么呢?

    • 一个是功能性上的(模块),比如提供横竖屏设置、数据库存储等
    • 一个是UI层面上的(UI组件),比如可以自定义一个VideoView视频播放器等

    就目前项目而言,详情页开发涉及到的两个正好是上面的两种情况,横竖屏设置(功能)和使用原生VideoView播放视频(UI)。

    原生模块之横竖屏设置功能开发

    在我们项目目录下XiFan/android 存放的是android项目的代码,需要使用Android Studio来开发,如果你不是android开发者,那么你可能需要先搭建android开发环境,这里就不介绍了,可自行网上搜索查阅资料。这里放一个工具下载地址 http://www.androiddevtools.cn/

    打开android项目之后(打开之后会弹窗提示更改gradle版本,点击忽略即可),目录结构如下:

    android项目结构

    module是新建的一个包名,专门存放自定义的组件。开发一个功能组件,需要配对实现一个Module和Package。

    实现JAVA端模块

    在module包下,新建OrientationModule类,并继承ReactContextBaseJavaModule

    public class OrientationModule extends ReactContextBaseJavaModule{
    
        public OrientationModule(ReactApplicationContext reactContext) {
            super(reactContext);
        }
    
        @Override
        public String getName() {
            return "Orientation";
        }
    
        @Nullable
        @Override
        public Map<String, Object> getConstants() {
            return null;
        }
    }

    getName方法返回的是该组件的名称(该名称可以加RCT前缀,如RCTOrientation,但JS中调用的还是没有加前缀的Orientation),在JS中供NativeModules使用;getConstants方法返回一组key/value常量属性值,可供JS中调用。具体看实现吧。

    首先我们需要提供一个方法,供JS中调用设置应用的横竖屏。要让JS中能调用,需要在方法上使用ReactMethod注解。

    @ReactMethod
    public void setOrientation(final int orientation){
        UiThreadUtil.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Activity activity = getCurrentActivity();
                if(activity == null){
                    return;
                }
                if(orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE){
                    activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                    //设置全屏
                    Window window = activity.getWindow();
                    WindowManager.LayoutParams params = window.getAttributes();
                    params.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
                    window.setAttributes(params);
    
                }else if(orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT){
                    activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                    //设置非全屏
                    Window window = activity.getWindow();
                    WindowManager.LayoutParams params = window.getAttributes();
                    params.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
                    window.setAttributes(params);
                }
            }
        });
    
    }

    这个方法接收一个orientation参数,只支持ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE和ActivityInfo.SCREEN_ORIENTATION_PORTRAIT常量参数值。由于react native是运行在JavaBridge线程,所以在使用android一些功能时,我们需要将在android UI线程里进行操作,因此这里调用UiThreadUtil.runOnUiThread。

    那么,我们如何让使用者更方便的使用setOrientation这个方便,而不用知道orientation参数要传什么具体值呢?

    这里我们就需要使用上面提到的getConstants方法来定义常量值了。

    private static final String ORIENTATION_LANDSCAPE_KEY = "LANDSCAPE";//横屏
    private static final String ORIENTATION_PORTRAIT_KEY = "PORTRAIT";//竖屏
    
    
    @Nullable
    @Override
    public Map<String, Object> getConstants() {//定义返回值常量
        Map<String,Object> constants = MapBuilder.newHashMap();
        constants.put(ORIENTATION_LANDSCAPE_KEY, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        constants.put(ORIENTATION_PORTRAIT_KEY, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        return constants;
    }

    定义了LANDSCAPE 和 PORTRAIT两个属性常量,并分别赋予值ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 和 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT

    写完了Module,我们需要再写Package。新建OrientationPackage类,并实现ReactPackage接口。

    public class OrientationPackage implements ReactPackage{
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            return Arrays.<NativeModule>asList(
                    new OrientationModule(reactContext)
            );
        }
    
        @Override
        public List<Class<? extends JavaScriptModule>> createJSModules() {
            return Collections.emptyList();
        }
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }

    OrientationModule是属于NativeModule,所以,我们在createNativeModules方法中返回我们已经实现了的OrientationModule。

    最后,我们需要将OrientationPackage注册到react native中,让它能识别到我们的Module。

    打开MainApplication类,并将OrientationPackage添加到ReactPackage

    public class MainApplication extends Application implements ReactApplication {
    
      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        protected boolean getUseDeveloperSupport() {
          return BuildConfig.DEBUG;
        }
    
        @Override
        protected List<ReactPackage> getPackages() {
          return Arrays.<ReactPackage>asList(
              new MainReactPackage(),
              new OrientationPackage()
          );
        }
      };
    
      @Override
      public ReactNativeHost getReactNativeHost() {
          return mReactNativeHost;
      }
    }

    在getPackages方法中增加了我们的OrientationPackage。

    接下来就是在JS中使用它了。

    实现对应的JS模块

    新建VideoPlayScene.js,用于播放视频的场景。我们需要在应用进入该页面时,横屏显示,退出时恢复竖屏。

    import React,{Component} from 'react';
    import {
        View,
        WebView,
        NativeModules
    } from 'react-native';
    var Orientation = NativeModules.Orientation;
    
    export default class VideoPlayScene extends Component{
        constructor(props){
            super(props);
        }
    
        componentWillMount(){
            Orientation.setOrientation(Orientation.LANDSCAPE);
        }
    
        componentWillUnmount(){
            Orientation.setOrientation(Orientation.PORTRAIT);
        }
    }

    我们需要import NativeModules这个模块

    var Orientation = NativeModules.Orientation;

    NativeModules点后面的Orientation就是我们在原生代码OrientationModule中getName返回的字符串值。

    在componentWillMount 和 componentWillUnmount生命周期中调用setOrientation来实现需求。其中Orientation.LANDSCAPE 和 Orientation.PORTRAIT 就是我们在OrientationModule$getConstants 定义的两个常量值。

    那如果在js中需要接收一个回调方法,那么原生代码需要怎么写呢?

    在OrientationModule在定义一个方法,并接收一个Callback参数

    @ReactMethod
    public void getRequestedOrientation(Callback callback){
        int orientation = getReactApplicationContext().getResources().getConfiguration().orientation;
        callback.invoke(orientation);
    }

    js中调用

    componentWillMount(){
        Orientation.getRequestedOrientation((orientation)=>{
            console.log('current orientation :'+ orientation);
        });
        //Orientation.setOrientation(Orientation.LANDSCAPE);
    }

    这样就完成了横竖屏功能的开发了(不好的是,自定义的Module在IDE下并不能代码提示,有知道如何让它提示的话,请告知一下哈),贴下OrientationModule.java的完整代码

    public class OrientationModule extends ReactContextBaseJavaModule{
        private static final String ORIENTATION_LANDSCAPE_KEY = "LANDSCAPE";//横屏
        private static final String ORIENTATION_PORTRAIT_KEY = "PORTRAIT";//竖屏
    
        public OrientationModule(ReactApplicationContext reactContext) {
            super(reactContext);
        }
    
        @Override
        public String getName() {
            return "Orientation";
        }
    
        @Nullable
        @Override
        public Map<String, Object> getConstants() {//定义返回值
            Map<String,Object> constants = MapBuilder.newHashMap();
            constants.put(ORIENTATION_LANDSCAPE_KEY, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            constants.put(ORIENTATION_PORTRAIT_KEY, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            constants.put("initOrientation",getReactApplicationContext().getResources().getConfiguration().orientation);
            return constants;
        }
    
        @ReactMethod
        public void setOrientation(final int orientation){
            UiThreadUtil.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Activity activity = getCurrentActivity();
                    if(activity == null){
                        return;
                    }
                    if(orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE){
                        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                        //设置全屏
                        Window window = activity.getWindow();
                        WindowManager.LayoutParams params = window.getAttributes();
                        params.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
                        window.setAttributes(params);
    
                    }else if(orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT){
                        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                        //设置非全屏
                        Window window = activity.getWindow();
                        WindowManager.LayoutParams params = window.getAttributes();
                        params.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
                        window.setAttributes(params);
                    }
                }
            });
    
        }
        @ReactMethod
        public void getRequestedOrientation(Callback callback){
            int orientation = getReactApplicationContext().getResources().getConfiguration().orientation;
            callback.invoke(orientation);
        }
    }

    总结

    本节我们完成了android的自定义模块,使RN能够使用原生提供的能力。而关于js如何向native发送命令以及native如何向js发送事件的问题,原生模块和原生UI组件它们的通信都是一样的,所以我们把这个问题留在下一节来讲述。

    展开全文
  • IFIX中一些常用功能的VBA代码:1.退出工作台2.IE浏览器打开网页3.打开帮助文档4.关闭虚拟键盘(需要copy文件)5.打开虚拟键盘(需要copy文件)6.检测机器分辨率7.改变字体大小8.检测机器颜色是不是32...声音报警原代码
  • 在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。下面上代码,把这些直接放到要转发的页面里就可以了:html...

    最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。

    总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。

    下面上代码,把这些直接放到要转发的页面里就可以了:

    html部分:

    //引进mshare.js点击弹出原生分享面板点击触发朋友圈分享点击触发发送给微信朋友

    js部分:

    下面是mshare.js的代码分享,把这些代码新建一个js文件放进去,然后在页面中引进就ok了。

    /** * 此插件主要作用是在UC和QQ两个主流浏览器 * 上面触发微信分享到朋友圈或发送给朋友的功能 */'use strict';var UA = navigator.appVersion; /** * 是否是 UC 浏览器 */var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0; /** * 判断 qq 浏览器 * 然而qq浏览器分高低版本 * 2 代表高版本 * 1 代表低版本 */var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0; /** * 是否是微信 */var wx = /micromessenger/i.test(UA); /** * 浏览器版本 */var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0; /** * 获取操作系统信息  iPhone(1)  Android(2) */var os = (function () {    var ua = navigator.userAgent;     if (/iphone|ipod/i.test(ua)) {        return 1;    } else if (/android/i.test(ua)) {        return 2;    } else {        return 0;    }}()); /** * qq浏览器下面 是否加载好了相应的api文件 */var qqBridgeLoaded = false; // 进一步细化版本和平台判断if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {    qq = 0;} else {    if (qq && qqVs < 5.4 && os == 2) {        qq = 1;    } else {        if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {            uc = 0;        }    }}/** * qq浏览器下面 根据不同版本 加载对应的bridge * @method loadqqApi * @param  {Function} cb 回调函数 */function loadqqApi(cb) {    // qq == 0     if (!qq) {        return cb && cb();    }    var script = document.createElement('script');    script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';    /**     * 需要等加载过 qq 的 bridge 脚本之后     * 再去初始化分享组件     */    script.onload = function () {        cb && cb();    };    document.body.appendChild(script);}/** * UC浏览器分享 * @method ucShare */function ucShare(config) {    // ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']    // 关于platform    // ios: kWeixin || kWeixinFriend;    // android: WechatFriends || WechatTimeline    // uc 分享会直接使用截图    var platform = '';    var shareInfo = null;    // 指定了分享类型    if (config.type) {        if (os == 2) {            platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';        } else if (os == 1) {            platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';        }    }    shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];    // android     if (window.ucweb) {        ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);        return;    }    if (window.ucbrowser) {        ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);        return;    }}/** * qq 浏览器分享函数 * @method qqShare */function qqShare(config) {    var type = config.type;    //微信好友 1, 微信朋友圈 8    type = type ? ((type == 1) ? 8 : 1) : '';    var share = function () {        var shareInfo = {            'url': config.url,            'title': config.title,            'description': config.desc,            'img_url': config.img,            'img_title': config.title,            'to_app': type,            'cus_txt': ''        };        if (window.browser) {            browser.app && browser.app.share(shareInfo);        } else if (window.qb) {            qb.share && qb.share(shareInfo);        }    };    if (qqBridgeLoaded) {        share();    } else {        loadqqApi(share);    }}/** * 对外暴露的接口函数 * @method mShare * @param  {Object} config 配置对象 */function mShare(config) {    this.config = config;    this.init = function (type) {        if (typeof type != 'undefined') this.config.type = type;        try {            if (uc) {                ucShare(this.config);            } else if (qq && !wx) {                qqShare(this.config);            }        } catch (e) {}    }}// 预加载 qq bridgeloadqqApi(function () {    qqBridgeLoaded = true;});if (typeof module === 'object' && module.exports) {    module.exports = mShare;} else {    window.mShare = mShare;}
    fc95eb6be4af4efd7033703bf8371b86.png
    /** * 此插件主要作用是在UC和QQ两个主流浏览器 * 上面触发微信分享到朋友圈或发送给朋友的功能 */'use strict';var UA = navigator.appVersion; /** * 是否是 UC 浏览器 */var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0; /** * 判断 qq 浏览器 * 然而qq浏览器分高低版本 * 2 代表高版本 * 1 代表低版本 */var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0; /** * 是否是微信 */var wx = /micromessenger/i.test(UA); /** * 浏览器版本 */var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0; /** * 获取操作系统信息  iPhone(1)  Android(2) */var os = (function () {    var ua = navigator.userAgent;     if (/iphone|ipod/i.test(ua)) {        return 1;    } else if (/android/i.test(ua)) {        return 2;    } else {        return 0;    }}()); /** * qq浏览器下面 是否加载好了相应的api文件 */var qqBridgeLoaded = false; // 进一步细化版本和平台判断if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {    qq = 0;} else {    if (qq && qqVs < 5.4 && os == 2) {        qq = 1;    } else {        if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {            uc = 0;        }    }}/** * qq浏览器下面 根据不同版本 加载对应的bridge * @method loadqqApi * @param  {Function} cb 回调函数 */function loadqqApi(cb) {    // qq == 0     if (!qq) {        return cb && cb();    }    var script = document.createElement('script');    script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';    /**     * 需要等加载过 qq 的 bridge 脚本之后     * 再去初始化分享组件     */    script.onload = function () {        cb && cb();    };    document.body.appendChild(script);}/** * UC浏览器分享 * @method ucShare */function ucShare(config) {    // ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']    // 关于platform    // ios: kWeixin || kWeixinFriend;    // android: WechatFriends || WechatTimeline    // uc 分享会直接使用截图    var platform = '';    var shareInfo = null;    // 指定了分享类型    if (config.type) {        if (os == 2) {            platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';        } else if (os == 1) {            platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';        }    }    shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];    // android     if (window.ucweb) {        ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);        return;    }    if (window.ucbrowser) {        ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);        return;    }}/** * qq 浏览器分享函数 * @method qqShare */function qqShare(config) {    var type = config.type;    //微信好友 1, 微信朋友圈 8    type = type ? ((type == 1) ? 8 : 1) : '';    var share = function () {        var shareInfo = {            'url': config.url,            'title': config.title,            'description': config.desc,            'img_url': config.img,            'img_title': config.title,            'to_app': type,            'cus_txt': ''        };        if (window.browser) {            browser.app && browser.app.share(shareInfo);        } else if (window.qb) {            qb.share && qb.share(shareInfo);        }    };    if (qqBridgeLoaded) {        share();    } else {        loadqqApi(share);    }}/** * 对外暴露的接口函数 * @method mShare * @param  {Object} config 配置对象 */function mShare(config) {    this.config = config;    this.init = function (type) {        if (typeof type != 'undefined') this.config.type = type;        try {            if (uc) {                ucShare(this.config);            } else if (qq && !wx) {                qqShare(this.config);            }        } catch (e) {}    }}// 预加载 qq bridgeloadqqApi(function () {    qqBridgeLoaded = true;});if (typeof module === 'object' && module.exports) {    module.exports = mShare;} else {    window.mShare = mShare;}

    好了,这样就可以直接唤起微信进行分享啦

    展开全文
  • Java编写的网页版魔方游戏 内容索引:JAVA源码,游戏娱乐,魔方,网页游戏 Java编写的网页版魔方游戏,编译后生成.class文件,然后用HTML去调用,不过运行时候需要你的浏览器安装有运行Class的插件。Java源代码实现部分...
  • 【倒置选区值】 将选中区域存储格的内容进行完全倒置,倒置的倒置即还原为内容,支持多行多列倒置。 【转置选区值】 将选中存储格区域内容自动识别行转置为列与列转置为行,支持多行多列相互转置并且能保留转置前的...
  • 序 曾经在一家公司有过这样的经历:...的错误信息﹐本想退出﹐但发现该页面的查询条件输入区域仍在﹐而且查询按钮也只是灰掉而已﹐在查看了网页原代码后﹐抱着随便试一下的心态﹐我在浏览器的地址栏里输入了一行...

    曾经在一家公司有过这样的经历:上班第一天﹐同事在公司的内部网帮我开了一个账号﹐要我登录公司的管理系统学习一下公司的管理制度。看完这些“文件”后,我随便点了一下系统左边的"员工信息查询"菜单﹐随即右边网页的数据区域显示"您无权查看此页"的错误信息﹐本想退出﹐但发现该页面的查询条件输入区域仍在﹐而且查询按钮也只是灰掉而已﹐在查看了网页原代码后﹐抱着随便试一下的心态﹐我在浏览器的地址栏里输入了一行js代码:javascript:alert(document.all['querybtn'].disabled=false) 使查询按钮启用﹐然后单击它﹐居然真的把人事基本资料给查了出来,随后我又打开这个系统的其它页面﹐发现都只是把动作按钮给disable掉来管理权限。

     

    当把人事薪资等非常敏感的资料放在web系统中时,如果只是通过上面这种方式来保证数据不被非法读取,很明显这个系统没有达到它应该达到的安全级别。

     

    作为一个web系统设计师,在规划一个系统时,必然会考虑到系统的安全性。如何有效的保证系统的安全,如何规划和实现一个可重用,可扩展的安全管控方案都是在安全管控时要考虑的主题。

     

    借着这个机会,笔者打算将自己从事web系统安全设计的经验和大家分享。从一个系统设计师的角度说明web系统安全管控。

    1.         web运作原理,您的系统到底有多安全?

    2.         权限抽象,还一个统一的权限接口

    3.         管控观念转换,柳岸花明又一村。

    4.         通用安全组件,从此不再苦海挣扎。

    Web运作原理

     

    Web是由客户端(Client)的请求(Request)和服务器(Web Server)的响应(Response)构成,同一个客户端的多次Request对于Web Server来说都一样,服务器不会将当前收到的Request和以往任何的Request联系起来,因为它们交互的依据是http协议,而此协议规定了http连接的无状态特征。

     

    以下为最一个最简单的Request请求:

    GET /TestWeb/test.htm HTTP/1.1

    Host: localhost

    Connection: close

     

    它表示向localhost主机请求路径为/TestWeb/test.htmhtml网页,使用GET方法, 1.1版本的HTTP协议。

     

    对此请求,WindowsIIS6.0是这样给出Response:

    HTTP/1.1 200 OK

    Content-Length: 12

    Content-Type: text/html

    Last-Modified: Wed, 05 Nov 2008 01:01:17 GMT

    Accept-Ranges: bytes

    Server: Microsoft-IIS/6.0

    Date: Wed, 05 Nov 2008 01:01:52 GMT

    Connection: close

     

    Hello World!

     

    包括响应状态码200body的长度,类型,所请求文件的最后修改日期等响应头(Response Header),还有简单的Hello World! 12个字符的html响应体(Response Body)

     

    Request请求不依赖于浏览器,事实上您可以使用任何程序语言通过网络编程来做到,以下是一个C#发送Request的例子:

     

    using System;

    using System.Text;

    using System.IO;

    using System.Net;

    using System.Net.Sockets;

    using System.Text.RegularExpressions;

     

    public class RequestDemo

    {

        //建立socket连接

        private static Socket ConnectSocket(string server, int port)

        {

            Socket s = null;

            IPHostEntry hostEntry = null;

            hostEntry = Dns.GetHostEntry(server);

            foreach (IPAddress address in hostEntry.AddressList)

            {

                IPEndPoint ipe = new IPEndPoint(address, port);

                Socket tempSocket =

                    new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                tempSocket.Connect(ipe);

                if (tempSocket.Connected)

                {

                    s = tempSocket;

                    break;

                }

                else

                    continue;

            }

            Console.WriteLine(s == null ? "" : "连接建立成功﹗");

            return s;

        }

     

        //发送request请求并接收响应字符串

        private static string SocketSendReceive(string request, string server, int port)

        {

            Byte[] bytesSent = Encoding.ASCII.GetBytes(request);

            Byte[] bytesReceived = new Byte[256];

            Socket s = ConnectSocket(server, port);

            if (s == null)

                return ("连接失败﹗");

            Console.WriteLine("正在发送请求...");

            s.Send(bytesSent, bytesSent.Length, 0);

            int bytes = 0;

            StringBuilder responsestr = new StringBuilder();

            Console.WriteLine("正在接收web服务器的回应...");

            do

            {

                bytes = s.Receive(bytesReceived, bytesReceived.Length, 0);

                responsestr.Append(Encoding.UTF8.GetString(bytesReceived, 0, bytes));

            }

            while (bytes > 0);

            return responsestr.ToString();

        }

     

        public static void Main(string[] args)

        {

           //读取在Request.txt中的Request字符串(request.txt末尾至少要留个空行,表明Request结束)

        string requeststr = File.ReadAllText("C:\\tmp\\request.txt")

        Console.WriteLine("请求字符串如下﹕\n{0}\n", requeststr;

     

            //发送且接收Response

            string result = SocketSendReceive(requeststr, "localhost", 80);

            Console.WriteLine("\n{0}", result);

            Console.ReadLine();

        }

    }

    注:C:\tmp\request.txt中的内容就是前面的Request字符串。

     

    程序执行的结果如下:

    r_Untitled-1.gif 

    除了直接进行Request外,大部分时候,我们在网页上单击某个链接或按钮时,浏览器和web服务器也在背后进行着这样的请求和响应。

     

    例如以下网页程序:

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="form.aspx.cs" Inherits="form" %>

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml" >

    <head runat="server">

        <title>测试窗体Request</title>

    </head>

    <body>

        <form id="form1" runat="server">

        <div>

            <asp:Label ID="Label1" runat="server" Text="你的名字:"></asp:Label>

            <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>

            <asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="送出" />

            <asp:Label ID="Label2" runat="server" ForeColor="OrangeRed"></asp:Label></div>

        </form>

    </body>

    </html>

     

    Aspx.cs代码如下:

    using System;

    using System.Data;

    using System.Configuration;

    using System.Collections;

    using System.Web;

    using System.Web.Security;

    using System.Web.UI;

    using System.Web.UI.WebControls;

    using System.Web.UI.WebControls.WebParts;

    using System.Web.UI.HtmlControls;

     

    public partial class form : System.Web.UI.Page

    {

        protected void Page_Load(object sender, EventArgs e)

        {

     

        }

        protected void Button1_Click(object sender, EventArgs e)

        {

            Label2.Text = "你输入的名字是:" + TextBox1.Text;

        }

    }

     r_Untitled-2.gif

     

    当输入“小生”并按钮“送出”按钮时,实际上就是发送下面的这样一段Request

    POST /TestWeb/form.aspx HTTP/1.1

    Cache-Control: no-cache

    Connection: close

    Content-Length: 206

    Content-Type: application/x-www-form-urlencoded

    Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*

    Accept-Encoding: gzip, deflate

    Accept-Language: zh-tw

    Cookie: ASP.NET_SessionId=jd14mp2k4e0dyga4hjz1zgby

    Host: localhost

    Referer: http://localhost/TestWeb/form.aspx

    User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)

    UA-CPU: x86

     

    __VIEWSTATE=%2FwEPDwUJODUwMjI0NzE3ZGQgpj%2Fm%2BSOD2vEbxBDW9BpDvogpgA%3D%3D&TextBox1=%E5%B0%8F%E7%94%9F&Button1=%E9%80%81%E5%87%BA&__EVENTVALIDATION=%2FwEWAwKdv8y5BwLs0bLrBgKM54rGBj5ZvpRog0Ox8f9YoKD3sYnCmNxG

     

    其中TextBox1后面的%E5%B0%8F%E7%94%9F就是“小生”(encodeURI函数编码的结果),修改此行数据,并对Content-Length作适当调整,就完全可以模仿按下送出“按钮的动作。

     

    实际上,不管是直接在地址栏输入url,还是在网页上单击链接,提交窗体,Ajax请求,web service呼叫等等,客户端都需要向web server发送相关的Request。因此对于管控比较全面的安全方案,必须对每一次Request都进行验证。而且这个验证最好在所有的被请求程序执行之前就完成,就好比您的web应用程序是一个大型游乐场,而您的安全管控就是这个游乐场唯一入口的检票处,凡进入游乐场的人员,都是买了票的,至于游乐场里面一些还要买票的项目,那就不属于入口检票处的责任了。

     

    可以对比一下手上的系统,看是否每次Request都有进行管控?对没有管控到的Request会不会发生问题?安全隐患的机率有多大?值不值得再加一次管控?

    您的系统有多安全?

     

    权限本质探讨

     

    所谓安全管控,其实就是权限判断,即对用户能否访问某一权限对象进行判断,并且在无权访问时的相应处理。

     

    权限,实际上就是使用者与权限对象的一种多对多的关系。

    如一个权限厂别的数据如下:

    UserID       FactID

    -------------------------

    1            1

    1            2

    2            3

    表示UserID1的用户拥有FactID12的权限,UserID2的用户拥有厂别ID3的权限。

     

    权限的本质在于对权限的使用方式进行抽象

    以下为常见的权限接口,主要包括判断权限和获取权限列表:

     

    /// <summary>

    /// 权限接口

    /// </summary>

    interface IRightProvider

    {

    /// <summary>

    /// 判断权限

    /// </summary>

    /// <param name="userID">用户ID</param>

    /// <param name="objectID">权限对象ID</param>

    /// <returns>true:有权限 false无权限</returns>

    bool HasRight(string userID, string objectID);

     

    /// <summary>

    /// 获取权限列表

    /// </summary>

    /// <param name="userID">用户ID</param>

    /// <returns>objectID列表</returns>

    List<string> GetRights(string userID);

    }

     

    判断权限HasRight方法

    使用示例如下:

    aspx.csPage_Load代码:

     

    string currentUserID = getUserID();        //获取当前登录用户

    string deleteUserRightID = "DeleteUser";    //删除用户的权限(HardCode删除用户的功能ID)

     

    IRightProvider functionRight;

    //实例化权限功能对象

    //如:functionRight = new FunctionRightProvider()

    ...

     

    //根据是否有权限,决定“删除”按钮是否显示

    if(functionRight.HasRight(currentUserID,deleteUserRightID))

          deleteBtn.Visible = true;

    else

          deleteBtn.Visible = false;

    ...

     

     

    获取权限列表GetRights方法

    使用示例如下:

    aspx.csPage_Load代码:

    string currentUserID = getUserID();                    //获取当前登录用户

     

    IRightProvider factRight;

    //实例化权限厂别对象

    ...

     

    //根据厂别ID获取权限厂别名称

    DataTable dt = GetRightData(factRight.GetRights(currentUserID));

     

    //将权限厂别数据绑定厂别下拉列表控件(这样用户只可以选择权限内的厂别)

    factDropDownList1.DataSource = dt;

    factDropDownList1.DataTextField = "FactName";

    factDropDownList1.DataValueField = "FactID";

    factDropDownList1.DataBind();

     

    上面这个例子,基于更安全的考虑,在Query按钮按下后,应该再对factDropDownList1.SelectedValue进行HasRight判断。

    实际开发中,也常常会将权限绑定和取值时的HasRight判断的代码整个封装成一个UserControl,这样就可以像普通DropDownList一样使用了。

     

    另外权限数据库设计,权限管理方式(分配和移除权限),群组策略,管理员策略等都是可以随系统大小来进行自定义的,当然最好给出一个比较稳定的方案,这样就不用每次都去考虑这个问题。

    以下为笔者常用权限架构方案:

     

    群组表:

    Groups(GroupID,GroupDesc,AppID)                    

    (AppID为系统ID,因为笔者的所用系统基本上共享一套权限管控方案。如果权限只是For单个系统建表,AppID可以省略,以下不再解释)

     

    群组成员表:

    GroupMembers (GroupID,UserID)

     

    用户权限表:

    UserRights(UserID,ObjectID,ObjectType,AppID)  

    ObjectType表示ObjectID是什么数据,如Fact表示是厂别IDFunction表示是功能ID。这样一次就可以满足多种权限的设定了

     

    群组权限表

    GroupRights(GroupID,ObjectID,ObjectType,AppID)

     

    管理员表

    UserAdmin(UserID,ObjectType,AppID)         

    某个权限类别的管理员,如有这样一笔数据 UserID:1,ObjectType:Fact。表明UserID1的用户拥有所有厂别权限

     

    有了这些架构,就可以轻松实现IRightProvider:

     

    /// <summary>

    /// 默认权限实做(简单的判断权限)

    /// </summary>

    class DefaultRightProvider : IRightProvider

    {

     

        IDataProvider _dataProvider;        //权限数据提供者

     

        public DefaultRightProvider(IDataProvider dataProvider)

        {

            _dataProvider = dataProvider;

        }

     

        #region IRightProvider 成员

     

        public bool HasRight(string userID, string objectID)

        {

            List<string> data = GetRights(userID);

            if (data != null && data.Contains(objectID))

                return true;

            return false;

        }

     

        public List<string> GetRights(string userID)

        {

            if (_dataProvider != null)

            {

                return new List<string>(_dataProvider.GetData(userID));

            }

            return null;

        }

     

        #endregion

    }

     

    /// <summary>

    /// 权限数据的读取策略

    /// </summary>

    interface IDataProvider

    {

        /// <summary>

        /// 获取某个用户的权限数据

        /// </summary>

        /// <param name="userID"></param>

        /// <returns></returns>

        IList<string> GetData(string userID);

    }

     

    /// <summary>

    /// 使用管理员,群组策略的权限机制

    /// </summary>

    class GroupAdminDataProvider : IDataProvider

    {

        string _appID;

        string _objectType;

     

        /// <summary>

        /// 某一系统,某一权限类别的权限数据访问接口

        /// </summary>

        /// <param name="appID"></param>

        /// <param name="objectType"></param>

        public GroupAdminDataProvider(string appID, string objectType)

        {

            _appID = appID;

            _objectType = objectType;

        }

     

        List<string> getGroupData(string groupID)

        {

            //调用数据访问层(或其它接口层),获取group的权限对象

        }

     

        List<string> getUserData(string userID)

        {

            //调用数据访问层(或其它接口层),获取user的权限对象

        }

     

        List<string> getAllData()

        {

            //调用数据访问层(或其它接口层),获取当前类别的所有对象

        }

     

        List<string> getUserGroup(string userID)

        {

            //调用数据访问层(或其它接口层),获取user所在的所有群组ID

        }

     

        public bool IsAdmin(string userID)

        {

            //调用数据访问层(或其它接口层),判断管理员权限

        }

     

        void addNoReplica(List<string> desList, List<string> addList)

        {

            if (addList != null && addList.Count > 0)

            {

                foreach (string addItem in addList)

                {

                    if (!desList.Contains(addItem))

                        desList.Add(addItem);

                }

            }

        }

     

     

        public IList<string> GetData(string userID)

        {

            if (IsAdmin(userID))

                return getAllData();

            else

            {

                List<string> ret = new List<string>();

     

                //加入本身权限

                List<string> userDatas = getUserData(userID);

                addNoReplica(ret, userDatas);

     

                //加入群组权限

                List<string> groups = getUserGroup(userID);

                if (groups != null)

                {

                    foreach (string groupID in groups)

                    {

                        List<string> groupDatas = getGroupData(groupID);

                        addNoReplica(ret, groupDatas);

                    }

                }

                return ret;

            }

        }

    }

     

    其中省略的部分都是通过对上述权限数据库的访问来完成。

     

    除了的HasRightGetRights接口外,涉及权限最多的就是权限管理了,包括群组建立,群组权限分配,用户加入群组,用户权限分配,用户管理员设定等等都可以封装成控件并且完成对上述权限数据库的增删改查即可,这样在以后涉及新权限时,权限管理部分都不用再额外投入开发成本了。

     

    这里特别的说明就是

    用户权限表:

    UserRights(UserID,ObjectID,ObjectType,AppID)  

    任何权限抽象成ObjectID统一了各种权限的处理,如果对于某种权限单凭一个ObjectID不好说明时,可以增加一个ObjectID的描述檔。例如有这样一种权限,用户在一个系统中对于不同的报表所拥有的权限不一样,有的只可以查询,有的却可以转Excel,有的还可以转PDF。这时候可以设计一个报表权限主文件

    ReportRightID     报表ID    动作方式

    1                订单报表   查询

    2                订单报表   Excel

    3                采购报表   PDF

     

    ObjectID存的就是ReportRightID了,当然这个报表权限主文件的增删改就是在分配权限时动态完成,以避免加入很多用不到的数据。

     

    判断权限时,需要增加一个动作,即先根据需要判断的报表ID和动作方式获取ReportRightID,再调用HasRight方法判断。而获取权限如某user能够查询的报表有哪些,某user对订单报表的权限动作有哪些,同样只要扩展GetRights方法就可完成(基于时间的关系,就不再深入。如果有兴趣,可以联系笔者再行探讨)

     

    上述工作完成后,判断权限就十分容易了,如厂别权限的判断:

     

    string FACTRIGHTTYPE = "FACT";      //权限类别

    //实例化权限数据获取器

    GroupAdminDataProvider dp = new GroupAdminDataProvider(appID, FACTRIGHTTYPE);

    //实例化权限类别

    IRightProvider factRight = new DefaultRightProvider(dp);

    factRight.HasRight(userID, factID); //判断权限

     

    有了这样互相支持,又相互独立的权限使用和权限管理分离理念后,权限使用就变得非常简单,权限架构又可以任意扩展,灵活变化,最终达到权限接口的统一使用。

     

    换一种安全管控观念

     

    先给出一段在aspx中常见的安全管控代码:

     

    假设登录后UserID存放在Session["UserID"]

     

    此页面是一个用户信息的修改删除页面,有两个按钮:

    SaveBtn    保存修改按钮,

    DeleteBtn  删除用户按钮

     

    有三个角色

    管理员:可以进行修改和删除

    主管:只可以修改

    普通用户:只能浏览

     

    以下为权限管控相关的代码片段:

     

    if(Session["UserID"] == null)

        Response.Redirect("Login.aspx");   //未登录转向登录页面

    string userID = Session["UserID"].ToString();

     

    //预设不能进行任何动作(普通登录用户只能看)

    SaveBtn.Visible = false;

    DeleteBtn.Visible = false;

     

    if(IsAdmin(userID)){         //如果用户是管理员,则可以修改和删除

        SaveBtn.Visible = true;

        DeleteBtn.Visible = true;

    }

    else if(IsManager(userID)){  //如果用户是主管,则只能修改

        SaveBtn.Visible = true;

    }

     

    顺便提一下:如果安全级别要求更高的话,在SaveBtn_ClickDeleteBtn_Click的事件中也要加入IsAdminIsManager的相关代码,以保证不被修改Request,来仿真这些按钮的动作(要验证可以将web运作原理一节中的form.aspx作相应修改,将送出按钮的Visible设为false,看是否可以触发Click事件就可知)

     

    这样的权限管控方式对于小型的,简单的系统可能能够满足,但是一旦系统比较大或者对权限的要求复杂一些时,就会发生问题:

     

    1:每个页面都需要写权限管控代码(即使能将这样的类似代码封装成一个方法,由于涉及到具体页面的具体控件,所以在aspx中还是少不了这样的权限管控代码)。

     

    2:权限代码与业务逻辑本质上是彼此无关,勉强放在一起会违反低耦合原则,因此在权限变动或修改时(例如增加一种角色,增加一个按钮)都可能会 影响到这些代码。

     

    3:IsAdmin,IsManager等都属于硬编码(hardcode)方式,这样一是在角色或权限变动时要重新修改代码,更重要的是无法动态管理权限(如动态分配角色的权限)。

     

    4:适用范围有限,基本上只适用于aspx文件,而如果系统需要管控xmljpgashxweb service等其它文件或程序的权限时,又要进行权限设计了。

     

    5:权限管控代码无法重用,每个新系统开发时都要重新考虑权限管控。

     

    要解决上面的这些问题,关键就是权限管控观念的转变:

    避免在业务程序中直接或间接加入权限判断,角色管控等代码(原因见上述第23)

    具体来讲,就是程序员在开发每一支程序时,就应该想到,user在操作这支程序时,他就应该已通过安全管控,而程序员在写这支程序时,也只要考虑如何实现这支程序要完成的功能就行。

     

    例如上面这个例子,权限管控与程序本身的显示,修改,删除逻辑纠缠在一起,耦合度过大,使得这两者在有变化时互相影响,增加程序的复杂性。如果转成这支程序只处理与业务逻辑有关的功能,成为上节中所讲的一种权限对象系统功能,这样就可以像普通的厂别权限一样进行统一的权限判断和管理了。

     

    从用户需求得知,有四个这样的系统功能需要实现,分别是新增,删除,修改和查看用户信息。

     

    那可能首先就会写四支程序,分别实现四个功能:

    查看用户:UserView.aspx

    修改用户:UserEdit.aspx

    删除用户:UserDelete.aspx 

    新增用户:UserAdd.aspx

     

    但是在开发过程中,发现前三支程序的代码差不多,因此重构改为一支程序,并且使用一个KindQueryString来区别

    查看使用者:User.aspx?Kind=View

    修改使用者:User.aspx?Kind=Edit

    删除使用者:User.aspx?Kind=Delete

     

    代码如下:

    string kind = Request.QueryString["kind"];

    if(kind==null)

         kind = "View";   //如果没有kind,则默认为只读

     

    ConfirmBtn.Visible = false;   //确定按钮

    DeleteBtn.Visible = false;    //删除按钮

     

    if(kind == "Edit")

        ConfirmBtn.Visible = true;   //如果是修改,显示确定按钮

    else if(kind=="Delete")

        DeleteBtn.Visible = true;    //如果是删除,显示删除按钮

     

    ...

     

    这样也可以完成相同的功能。

     

    随着需求进一步明确,user提出希望管理员能够在同一个页面中进行修改和删除用户。

     

    因为功能变得可以组合,所以要换一下Kind的定义方式,Kind分为201字符,分别表示删除和修改功能,如:

    User.aspx?Kind=01

    表示这支程序实现的功能是修改用户,而

    User.aspx?Kind=11

    则表示这支程序实现的功能是新增和修改。

    代码如下:

    string kind = Request.QueryString["kind"];

    if(kind==null)

         kind = "00";   //如果没有kind,则默认为只读

     

    ConfirmBtn.Visible = false;   //确定按钮

    DeleteBtn.Visible = false;    //删除按钮

     

    bool isDelete = kind[0]=='1';

    bool isEdit = kind[1]=='1';

     

    if(isEdit)

        ConfirmBtn.Visible = true;   //如果有修改功能,显示确定按钮

    if(isDelete)                                        //改为if,而不是else if,这样功能可任意组合

        DeleteBtn.Visible = true;    //如果有删除功能,显示删除按钮

     

    ...

     

    最后实现的系统功能有:

    用户管理       User.aspx?Kind=11

    用户修改       User.aspx?Kind=01

    用户查看       User.aspx

    新增用户       UserAdd.aspx

     

    这样就完成了权限管控之前的准备工作了,即将系统功能看作是一种权限对象,到时再对这种权限对象进行相应的管理和判断即可。

     

    接下来的事交由权限管控组件统一进行了

     

    可能有的读者会认为如果修改和删除权限动态组合怎么办,即有的user只可以删除,有的user只可以修改,有的user可删可改,而且如果一支程序实现了很多的功能组合,那这边的系统功能是不是很难产生?

     

    的确,会有这种问题,可以通过以下方式解决:

    1:进一步明确需求,看是否真的有随意的权限组合这样的需求。像上面这支程序,真实应用中可能就只有两种权限,管理员可以增,删,改,查,而普通用户则只可以查询。这样就可以静态的添加两个系统功能及其对应的程序。

    2:而如果用户确实存在系统运行时权限动态变化的可能时,那也只要再加强权限分配程序,当在给用户分配系统功能时,提供相关系统功能的子选项(如:用户信息功能,有查看,有删除,可修改的子功能)让用户自行勾选,然后程序再动态的增加系统功能与程序的对应就行。

     

    不管如何,由于统一了权限使用接口,抽象了权限对象的观念,因此使得上层的权限判断代码能够保持稳定,而共享权限判断组件。

     

    在实际开发过程中,大部分都没有这么复杂,往往一支程序就实现一个系统功能,或者多支程序共同实现一个系统功能,或多个系统功能需要一支程序来实现(程序中不需要区分不同功能,如一些共享程序)。这样在程序中连进行最简单的区分不同功能的代码都可以省下,转为纯粹的业务逻辑实现,更好地实现安全管控

     

    类似的,其它各种的需要权限管控的资源,如web service,ashx,xml,jpg等都可以建立类的对应。为了方便,可以灵活运用多种对应方式,如整个目录对应到一个系统功能,省却将一支支程序加入数据库的麻烦。

     

    换一个角度,转变一下观念,正是山穷水复疑无路,柳岸花明又一村

     

    通用安全组件设计

    有了权限统一接口,有了新的web安全管控思维,接下来的安全组件设计也就顺理成章了,

    安全管控组件其实就是一个管控流程类(SecurityProvider),而管控流程(Valid)也就是:认证à写入UserIDà授权à拒绝访问处理。

     

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Web;

     

    namespace WebSecurity

    {

        /// <summary>

        /// 安全管控流程提供者

        /// </summary>

        public class SecurityProvider

        {

     

            /// <summary>

            /// 进行安全管控(管控流程:认证->UserID->授权->拒绝访问处理)

            /// </summary>

            /// <param name="context"></param>

            public void Valid()

            {

                string userid = authenticate();      //认证(获取user id)

                if (userid != null)

                    setUserID(userid);               //UserID

                if (!authroize(userid))              //授权(是否可以访问)

                    forbidAction();                  //拒绝访问处理

            }

     

            /// <summary>

            /// 识别当前RequestUser ID

            /// </summary>

            /// <returns>

            /// 返回当前RequestUser ID,如果未登录,则返回null

            /// </returns>

            string authenticate()

            {

                

            }

     

            /// <summary>

            /// 与应用程序的接口,指定UserID存放地点(如:Sessioncontext.Items["UserID"]),以便应用程序使用

        /// </summary>

           /// <param name="context"></param>

            /// <param name="userid"></param>

            void setUserID(string userid)

       {

     

       }

     

           /// <summary>

            /// 对当前用户(包括匿名用户)进行授权

            /// </summary>

            /// <param name="context">可以匿名访问的URL也在这里处理</param>

            /// <param name="userid"></param>

            /// <returns></returns>

            bool authroize(string userid)

            {

     

            }

     

            /// <summary>

            /// 拒绝访问采取的措施(如转向登录页面,提示无权登录信息,输出soap error message)

            /// </summary>

            /// <param name="context"></param>

            void forbidAction()

            {

     

            }

        }

    }

     

    1.认证

    认证就是识别当前发出请求(Request)的用户,相对于程序来说,就是UserID

    识别UserID,与具体系统,具体程序类型有关。

    一般通过浏览器直接发出的Request,其UserID的获取与系统的登录程序相关,如登录后UserID存在SessionCookie中,那在这里获取UserID就是通过SessionCookie

    如下面这个类可以提供给登录aspx和这里的认证方法共享,其中登录时调用LoginInPage方法,而在这里的认证中就可以调用相应的GetUserID方法。

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Web;

     

    namespace WebSecurity

    {

     

     

        class LoginHelper

        {

     

            /// <summary>

            /// 登录状态码

            /// </summary>

            enum LoginStatus

            {

                Success,            //成功

                FirstLogin,         //成功,但是首次登录,客户程序可以选择导向修改密码程序

     

                NoApproval,         //账号还未审核(未启用)

                AccountNotExists,   //账号不存在

                PasswordInvalid,    //密码错误(为了防止hack,也可以不提供这么明确的错误信息但这个可以给客户程序选择)

                AccountDisabled     //账号被停用

            }

     

     

            /// <summary>

            /// 用户在网页上登录(需要写入登录票据)

            /// </summary>

            /// <param name="account">账号</param>

            /// <param name="password">密码</param>

            /// <param name="statusCode">登录状态</param>

            /// <return>登录状态</return>

            public LoginStatus LoginInPage(string account, string password)

            {

                LoginStatus retStatus;

                string retUserID = Login(account, password, out retStatus);

                if (retUserID != null)

                    writeUserID(retUserID);     //写入UserID

                return retStatus;

            }

     

            /// <summary>

            /// 登录

            /// </summary>

            /// <param name="account">账号</param>

            /// <param name="password">密码</param>

            /// <param name="statusCode">登录状态</param>

            /// <returns>返回UserID,null表示登录失败</returns>

            public string Login(string account, string password, out LoginStatus statusCode)

            {

                //一般是访问数据库以判断账号密码是否OK,以及其它登录管控策略(如被停用等等)

            }

     

            public static readonly string UserIDKey = "UserID";

     

            /// <summary>

            /// 写入登录UserID

            /// </summary>

            /// <param name="userID"></param>

            /// <returns></returns>

            void writeUserID(string userID)

            {

                //Session示例

                HttpContext.Current.Session.Add(UserIDKey, userID);

            }

     

            /// <summary>

            /// 注销

            /// </summary>

            public void Logout()

            {

                clearUserID();

            }

     

            /// <summary>

            /// 清除登录票据

            /// </summary>

            /// <returns></returns>

            void clearUserID()

            {

                //Session示例

                if (GetUserID() != null)

                    HttpContext.Current.Session.Remove(UserIDKey);

            }

     

            /// <summary>

            /// 获取登录成功后写入的UserID

            /// </summary>

            /// <returns></returns>

            public string GetUserID()

            {

                string ret = null;

                //Session示例

                if (HttpContext.Current.Session != null && HttpContext.Current.Session[UserIDKey] != null)

                    ret = (string)HttpContext.Current.Session[UserIDKey];

                return ret;

            }

        }

    }

    对于一些比较特别的Request,有其自己UserID获取方式。如Web Service,可能UserID会以加密方式存放在SoapHeader中,也有可能直接在SoapHeader中传送账号密码,而在认证过程中调用Login方法来取得当前UserID.

    string authenticate()

    {

        HttpRequest request = HttpContext.Current.Request;

        string lowerNoQueryPath = request.Url.PathAndQuery.Split('?')[0].ToLower();

        LoginHelper loginHelper = new LoginHelper();

        if (lowerNoQueryPath.EndsWith(".aspx"))

            return loginHelper.GetUserID();

        else if (lowerNoQueryPath.EndsWith(".asmx"))

        {

            //RequestBody中解析Soap头,读取useridpassword

            //然后调用loginHelper.Login方法登录

     

        }

    }

    此方法内代码可以灵活编写,以方便重用,动态加载等。

     

    2.写入UserID

    安全管控组件由于与应用系统无关,因此必须提供一种统一的方式以便各种程序(aspx,asmx)在自己的代码中获取userID

    void setUserID(string userid)

    {

    HttpContext.Current.Items.Add("UserID", userid);

    }

    3.授权

    授权的代码较为简单,基本上就循环判断当前Request对应的系统功能就行。这里最重要的就是识别Request的系统功能,并依照各种规则映射到系统功能上(特别注意,如果请求的是无权限时转向的url,则一定要返回true,否则会形成死循环)

    bool authroize(string userid)

    {

    //不同的方式可以不同的处理

    //以下为伪码实现

    HttpRequest request = HttpContext.Current.Request;

    string lowerNoQueryPath = request.Url.PathAndQuery.Split('?')[0].ToLower();

    if (lowerNoQueryPath.EndsWith("login.aspx"))

    return true;

     

     

    AppUrl url = getUrl(HttpContext.Current.Request);  //获取当前的url对象

    if (url.MustAuth == "N")    //如果这支程序不需要授权,则通过

    return true;

    else if (userid == null)      //挡掉匿名登录者

    return false;

    string appid = getCurrentAppID();  //获取当前访问的AppID;

    IRightProvider functionProvider = new DefaultRightProvider(new GroupAdminDataProvider(appid, "FUNCTION"));   //Function表示系统功能类别的权限

    List<string> functions = url.GetMapFunctions();   //获取url所对应的Function ID列表

    if (functions != null)

    {

    //只要拥有任何一个所对应的系统功能权限则放行

    foreach (string funid in functions)

    {

    if (functionProvider.HasRight(userid, funid))

    return true;

    }

    }

    return false;

    }

    4.拒绝访问处理

    至于拒绝访问,也需要针对不同的类型进行处理,如:

    void forbidAction()

    {

                HttpRequest request = HttpContext.Current.Request;

                string lowerNoQueryPath = request.Url.PathAndQuery.Split('?')[0].ToLower();

                LoginHelper loginHelper = new LoginHelper();

                if (lowerNoQueryPath.EndsWith(".aspx"))

                    HttpContext.Current.Response.Redirect("Login.aspx");     //转向的无权页面一定要和authorize方法结合起来,避免形成死循环

                else if (lowerNoQueryPath.EndsWith(".asmx"))

                {

                    //可引发soap 异常报告无权登录

     

                }          

    }

     

    在将SecurityProvider组件设计OK后,就需要选择在何时调用该组件进行管控了,asp.netHttpModule机制刚好提供了这样的管控时机,实做一个HttpModule,捕捉一个真正的Request程式执行之前的事件如PreRequestHandlerExecute中调用SecurityProvider.Valid方法进行管控即可。

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Web;

     

    namespace WebSecurity

    {

        public class SecurityModule : IHttpModule

        {

            //安全管控对象

            SecurityProvider _provider;

     

            public SecurityModule()

            {

                _provider = new SecurityProvider();

            }

     

            public void Init(HttpApplication application)

            {

                //捕获PreRequest事件

                application.PreRequestHandlerExecute += new EventHandler(application_PreRequestHandlerExecute);

            }

     

            void application_PreRequestHandlerExecute(object sender, EventArgs e)

            {

                //每次请求都进行权限管控

                _provider.Valid();

            }

     

            public void Dispose()

            {

            }

     

        }

    }

    然后在Web.Config中配置即可完成权限管控

    <httpModules>

          <add name="WebSecurity" type="WebSecurity.SecurityModule,WebSecurity"/>

    </httpModules>

     

    当然如果简单点,也可以在Global.asax中捕获事件进行SecurityProvider.Valid验证。

     

    当安全组件建立后,每个系统的开发,都不再需要考虑权限管控,而是只要实现系统本身的业务逻辑和功能。通过后期的权限管控配置,系统功能与程式对应关系的建立,权限分配控件的灵活使用,最终脱离“苦海”,不再挣扎。



    ----------------------------------

    2011.1.22补录

    这篇文章的一些看法已过时

    验证和授权并不是每次访问的必需,如有的网页可匿名访问时,每次解码cookie有点多余也影响性能

    至于验证,如userID则应该是一个工具方法,按需进行

    当然大部分需要授权的模块,可以在需要进行验证时才进行

    如有的授权可以对所有用户,所有内网用户,公司用户等等时,就没必要验证userID来浪费

    以后有时间再专门提出关于验证和授权的作法

    展开全文
  • 标题:Python 获取微信好友地区、性别、签名信息并将结果可视化根据itchat库获得好友信息,使用下面两行代码可以做到: 第一行代码是使用itchat库登录微信网页版,它会弹出扫码登录界面,设置参数hotReload=True的...
  • cookie的设置

    2020-12-26 02:19:37
    我通过你的demo测试发现我把原生cookie设置的相关代码去掉,在wkwebview的一个页面,网页设置了cookie,退出后,重新进入浏览器还是可以有cookie的,但是如果你的网页不是通过...
  • (原来那个网页找不到了,没办法放链接了,感谢作者) #实现一个人力资源管理员工管理(员工编号+姓名)的程序。 info="功能:1入职,2离职,3修改,4查看所有员工信息,5搜索指定员工,6重置,7退出" #员工编号...
  • 精易模块[源码] V5.15

    2015-03-21 22:03:37
    3,网页访问_对象()增加一个参数,用来保存网页返回状态代码。 4、改善(字节集_取左边|取右边|取中间)与未公开子程序重复,改名为 字节集_寻找取左|字节集_寻找取右|字节集_寻找取中,并修正BUG。 5、公开子程序...
  • java源码包---java 源码 大量 实例

    千次下载 热门讨论 2013-04-18 23:15:26
     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...
  • java源码包2

    千次下载 热门讨论 2013-04-20 11:28:17
     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...
  • java源码包3

    千次下载 热门讨论 2013-04-20 11:30:13
     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...
  • java源码包4

    千次下载 热门讨论 2013-04-20 11:31:44
     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...
  • Java编写的网页版魔方游戏 内容索引:JAVA源码,游戏娱乐,魔方,网页游戏 Java编写的网页版魔方游戏,编译后生成.class文件,然后用HTML去调用,不过运行时候需要你的浏览器安装有运行Class的插件。Java源代码实现部分...
  • Java编写的网页版魔方游戏 内容索引:JAVA源码,游戏娱乐,魔方,网页游戏 Java编写的网页版魔方游戏,编译后生成.class文件,然后用HTML去调用,不过运行时候需要你的浏览器安装有运行Class的插件。Java源代码实现部分...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...
  • 项目: electron仿客户端QQ简易版 简单讲解electron的起源 --- 学习 -- 从入门到放弃! 启动 npm install npm run dev ...技术栈:electron + vue全家桶 + elementUi + frozenui ...简单传值代码地址
  • 网页模版清新简洁。全功能无任何限制,绿色实用,功能不算强大,但很实用。 2、模板采用html css架构,兼容世界之窗/360安全/360极速/FF/IE789及以上等主流浏览器. 3、主页模板主体布局清晰实用,分为三大部分...
  • 世界之窗 3.5.0.3

    2011-09-28 17:18:49
    1.优化了部分启动代码,进一步提升主框架打开速度; 2.优化了flash加载机制,提升了没有flash元素页面的加载速度; 3.修复了点开菜单栏中的菜单选项后,点击其他插件按钮时,产生的闪屏问题; 4.修复了视频独立播放...
  • 其中callBack是一个回调,如webview的加载完成、向上滑动、向下滑动等等,代码如下: ///加载 Html 的回调 ///[code]消息类型标识 ///[msg] 消息内容 ///[content] 回传的参数 void callBack(int code, String ...
  • TheWorld v3.5 Final

    2011-07-02 16:29:24
    2.优化了部分启动代码,进一步提升主框架打开速度; 3.优化了flash加载机制,提升了没有flash元素页面的 加载速度; 4.修复了点开菜单栏中的菜单选项后,点击其他插件按 钮时,产生的闪屏问题; 5.修复了视频独立播放窗口...
  • Android 一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导开发 IOS 由苹果公司开发的移动操作系统 Webkit 一个开源的浏览器引擎,在手机上的...

空空如也

空空如也

1 2 3 4
收藏数 65
精华内容 26
关键字:

网页退出原代码