十年磨一劍--從程序員到架構師

一个.net程序员,一个企业应用的开发者,喜欢系统架构,数据库,领域驱动,面向对象,表现层技术。关注重用的理论和实践。设计原则:简单,快速,适应变化能力强,表现层灵活多变...

博客园 首页 新随笔 联系 订阅 管理

曾经在一家公司有过这样的经历:上班第一天﹐同事在公司的内部网帮我开了一个账号﹐要我登录公司的管理系统学习一下公司的管理制度。看完这些“文件”后,我随便点了一下系统左边的"员工信息查询"菜单﹐随即右边网页的数据区域显示"您无权查看此页"的错误信息﹐本想退出﹐但发现该页面的查询条件输入区域仍在﹐而且查询按钮也只是灰掉而已﹐在查看了网页原代码后﹐抱着随便试一下的心态﹐我在浏览器的地址栏里输入了一行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字符串。

 

程序执行的结果如下:

 

除了直接进行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;

    }

}

 

 

当输入“小生”并按钮“送出”按钮时,实际上就是发送下面的这样一段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来浪费

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

posted on 2008-11-17 09:55  Kevin Zou  阅读(1929)  评论(7编辑  收藏  举报