Diligent achievement genius ...

业精于勤荒于嬉 行成于思毁于随 voiow博客
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

如何设计一个安全的WEBSERVICE

Posted on 2008-04-21 17:57  voiow  阅读(528)  评论(0编辑  收藏  举报
[ Team LiB ] Previous Section Next Section

Securing an XML Web Service

If you want to build a subscription Web service, you need some method of identifying your subscribers. Currently, there is no standard method of authenticating users of a Web service. So, to identify them, you have to be creative. This section explores one approach to securing an XML Web service.

NOTE

There are a number of security specifications currently in the works that will provide standard ways to pass user credentials and encrypt Web service transactions. For example, the WS-Security specification adds a <Security> element to the SOAP header so that there is a standard way to pass security credentials when accessing a Web service. To learn more about WS-Security and related specifications, visit msdn.microsoft.com/WebServices.


Overview of the Secure XML Web Service

For this example, you'll build a simple XML Web service that enables users to retrieve a number. This Web service will have one method named GetLuckyNumber(), which always returns the number 7. The goal is to prevent unauthorized users from successfully calling the GetLuckyNumber() method.

One approach to preventing unauthorized access to the GetLuckyNumber() method would be to force every user to pass a username and password parameter whenever the method is called. However, if the Web service has multiple methods, this requirement could quickly become cumbersome. You would need to add the username and password parameters to each and every method.

Our approach will be to use a custom SOAP header to pass authentication information. You can pass a SOAP header as part of the SOAP packet being transmitted to the Web service and verify the authentication information in the SOAP header with every method call.

You could simply pass a username and password in the SOAP header. However, doing so would be dangerous. SOAP packets are transmitted as human-readable XML documents across the Internet. If you transmit a password in a SOAP header, the password could be intercepted and read by the wrong person.

To protect the integrity of user passwords, you could use the Secure Sockets Layer (SSL) to encrypt every message sent to the Web service. However, using SSL has a significant impact on the performance of a Web server. It takes a lot of work to encrypt and decrypt each message.

Instead, you need to add only one method to the Web service that requires SSL. Create a single method named Login() to accept a username and password and return a session key that is valid for 30 minutes. The session key is passed in the SOAP header to every other method call to authenticate the user.

The advantage of this approach is that it is reasonably secure and does not have a significant impact on performance. The user needs to use SSL only when calling the Login() method. After the Login() method is called, only the session key, not the user password, is transmitted when calling other Web methods.

If someone manages to steal the session key, the session key will be valid for less than 30 minutes. After 30 minutes elapse, the key will be useless.

Creating the Database Tables

This secure XML Web service will use two Microsoft SQL Server database tables named WebServiceUsers and SessionKeys. The WebServiceUsers database table contains a list of valid usernames and passwords. You can create this database table by using the SQL CREATE TABLE statement contained in Listing 23.9.

Listing 23.9 CreateWebServiceUsers.sql
CREATE TABLE WebServiceUsers (
            userid int IDENTITY NOT NULL ,
            username varchar (20)  NOT NULL ,
            password varchar (20)  NOT NULL ,
            role int NOT NULL )
            

The C# version of this code can be found on the CD-ROM.

After you create the WebServiceUsers table, you should add at least one username and password to the table (for example, Steve and Secret).

When a user logs in, a session key is added to the SessionKeys database table. The SQL CREATE TABLE statement for the SessionKeys table is contained in Listing 23.10.

Listing 23.10 CreateSessionKeys.sql
CREATE TABLE SessionKeys (
            session_key  uniqueidentifier ROWGUIDCOL  NOT NULL ,
            session_expiration datetime NOT NULL ,
            session_userID int NOT NULL ,
            session_username varchar(20) NOT NULL ,
            session_role int NOT NULL )
            

The C# version of this code can be found on the CD-ROM.

Creating the Login() Method

The Web service will have a method named Login() that users must access once to retrieve a valid session key. Because passwords are passed to the Login() method, this method should be accessed only through a secure channel (with https:// rather than http://).

The complete code for the Login() method is contained in Listing 23.11.

Listing 23.11 The Login() Method
<WebMethod()> Public Function Login( username As String, password As String ) As ServiceTicket
            Dim conMyData As SqlConnection
            Dim cmdCheckPassword As SqlCommand
            Dim parmWork As SqlParameter
            Dim intUserID As Integer
            Dim intRole As Integer
            Dim objServiceTicket As ServiceTicket
            Dim drowSession As DataRow
            ' Initialize Sql command
            conMyData = New SqlConnection( "Server=localhost;UID=sa;pwd=secret; database=myData" )
            cmdCheckPassword = New SqlCommand( "CheckPassword", conMyData )
            cmdCheckPassword.CommandType = CommandType.StoredProcedure
            ' Add parameters
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@validuser", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.ReturnValue
            cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@username", username ) )
            cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@password", password ) )
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@sessionkey", SqlDbType.UniqueIdentifier ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@expiration", SqlDbType.DateTime ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@userID", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@role", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.Output
            ' Execute the command
            conMyData.Open()
            cmdCheckPassword.ExecuteNonQuery()
            objServiceTicket = New ServiceTicket
            If cmdCheckPassword.Parameters( "@validuser" ).Value = 0 Then
            objServiceTicket.IsAuthenticated = True
            objServiceTicket.SessionKey = cmdCheckPassword.Parameters ( "@sessionkey" ).Value
            graphics/ccc.gif.ToString()
            objServiceTicket.Expiration = cmdCheckPassword.Parameters ( "@expiration" ).Value
            intUserID = cmdCheckPassword.Parameters( "@userID" ).Value
            intRole = cmdCheckPassword.Parameters( "@role" ).Value
            Else
            objServiceTicket.IsAuthenticated = False
            End If
            conMyData.Close()
            ' Add session to cache
            If objServiceTicket.IsAuthenticated Then
            If Context.Cache( "SessionKeys" ) Is Nothing Then
            LoadSessionKeys
            End If
            drowSession = Context.Cache( "SessionKeys" ).NewRow()
            drowSession( "session_key" ) = objServiceTicket.SessionKey
            drowSession( "session_expiration" ) = objServiceTicket.Expiration
            drowSession( "session_userID" ) = intUserID
            drowSession( "session_username" ) = username
            drowSession( "Session_role" ) = intRole
            Context.Cache( "SessionKeys" ).Rows.Add( drowSession )
            End If
            ' Return ServiceTicket
            Return objServiceTicket
            End Function
            

The C# version of this code can be found on the CD-ROM.

The Login() method does the following:

  • Executes a SQL stored procedure named CheckPassword

  • Adds the current session to the cache

  • Returns a valid session key

The SQL CREATE PROCEDURE statement for the CheckPassword procedure is contained in Listing 23.12.

Listing 23.12 CreateCheckPassword.sql
Create Procedure CheckPassword
            (
            @username varchar( 20 ),
            @password varchar( 20 ),
            @sessionkey uniqueidentifier Output,
            @expiration DateTime Output,
            @userID int Output,
            @role int Output
            )
            As
            Select
            @userID = userid,
            @role = role
            From WebServiceUsers
            Where username = @username
            And password = @password
            If @userID Is Not Null
            Begin
            SET @sessionkey = NEWID()
            SET @expiration = DateAdd( mi, 30, GetDate() )
            Insert SessionKeys
            (
            session_key,
            session_expiration,
            session_userID,
            session_username,
            session_role
            ) values (
            @sessionkey,
            @expiration,
            @userID,
            @username,
            @role
            )
            End
            Else
            Return –1
            

The C# version of this code can be found on the CD-ROM.

The CheckPassword stored procedure checks whether the username and password passed to the procedure exist in the WebServiceUsers table. If the username and password combination is valid, a new session key is generated. The SQL NEWID() function then generates a Globally Unique Identifier (GUID) to use as the session key.

The stored procedure also generates the expiration time for the session key. The DataAdd() function returns a time that is 30 minutes in the future.

Both the GUID and expiration time are added as part of a new record to the SessionKeys database table, which tracks all the active current sessions.

After the Login() method calls the CheckPassword stored procedure, the method constructs a new instance of the ServiceTicket class. The declaration for this class is contained in Listing 23.13.

Listing 23.13 The ServiceTicket Class
Public Class ServiceTicket
            Public IsAuthenticated As Boolean
            Public SessionKey As String
            Public Expiration As DateTime
            End Class
            

The C# version of this code can be found on the CD-ROM.

The Login() method returns an instance of the ServiceTicket class, which contains the validated session key, session key expiration time, and a value indicating whether the user was successfully authenticated.

Retrieving the Custom SOAP Header

The XML Web service uses a custom SOAP header to retrieve authentication information. The session key is passed in a SOAP header named AuthHeader. You create the actual SOAP header by using the class in Listing 23.14.

Listing 23.14 The SOAP Header
Public Class AuthHeader:Inherits SoapHeader
            Public SessionKey As String
            End Class
            

The C# version of this code can be found on the CD-ROM.

Notice that this class inherits from the SoapHeader class. It contains a single property named SessionKey that is used to store the session key.

NOTE

The SoapHeader class inhabits the System.Web.Services.Protocols namespace. You need to import this namespace before you can use the SoapHeader class.


To use the AuthHeader class in your Web service, you need to add a public property that represents the header. You declare a public property in your Web service class like this:


            
Public AuthenticationHeader As AuthHeader
            

Finally, if you want to retrieve this header in any particular method, you must add the SoapHeader attribute to the method. The GetLuckyNumber() method in Listing 23.15 illustrates how to use this attribute.

Listing 23.15 The GetLuckyNumber() Method
<WebMethod(), SoapHeader( "AuthenticationHeader" )> _
            Public Function GetLuckyNumber As Integer
            If Authenticate( AuthenticationHeader ) Then
            Return 7
            End If
            End Function
            

The C# version of this code can be found on the CD-ROM.

The GetLuckyNumber() method simply passes AuthenticationHeader to a function named Authenticate(). This function is described in the next section.

Authenticating the Session Key

Every time someone calls the GetLuckyNumber() method, you could check the user's session key against the SessionKeys database table. However, with enough users, checking every session key would be slow. Instead, you can check the session key against a cached copy of the database table.

The private Authenticate() function in Listing 23.16 checks the session key retrieved from the authentication header against a cached copy of the SessionKeys database table.

Listing 23.16 The Authenticate() Function
Private Function Authenticate( objAuthenticationHeader ) As Boolean
            Dim arrSessions As DataRow()
            Dim strMatch As String
            ' Load Session keys
            If Context.Cache( "SessionKeys" ) Is Nothing Then
            LoadSessionKeys
            End If
            ' Test for match
            strMatch = "session_key='" & objAuthenticationHeader.SessionKey
            strMatch &= "' And session_expiration > #" & DateTime.Now() & "#"
            arrSessions = Context.Cache( "SessionKeys" ).Select( strMatch )
            If arrSessions.Length > 0 Then
            Return True
            Else
            Return False
            End If
            End Function
            End Class
            

The C# version of this code can be found on the CD-ROM.

Caching the Session Keys

As I mentioned previously, the SessionKeys database table is cached in memory to improve performance. The table is cached with the LoadSessionKeys subroutine contained in Listing 23.17.

Listing 23.17 The LoadSessionKeys Subroutine
Private Sub LoadSessionKeys
            Dim conMyData As SqlConnection
            Dim dadMyData As SqlDataAdapter
            Dim dstSessionKeys As DataSet
            conMyData = New SqlConnection( "Server=localhost;UID=sa;PWD=secret; database=myData" )
            dadMyData = New SqlDataAdapter( "LoadSessionKeys", conMyData )
            dadMyData.SelectCommand.CommandType = CommandType.StoredProcedure
            dstSessionKeys = New DataSet
            dadMyData.Fill( dstSessionKeys, "SessionKeys" )
            Context.Cache.Insert( _
            "SessionKeys", _
            dstSessionKeys.Tables( "SessionKeys" ), _
            Nothing, _
            DateTime.Now.AddHours( 3 ), _
            TimeSpan.Zero )
            End Sub
            

The C# version of this code can be found on the CD-ROM.

The SessionKeys table is cached in memory for a maximum of three hours. The table is dumped every three hours to get rid of expired session keys. When the table is automatically reloaded into the cache, only fresh session keys are retrieved.

The LoadSessionKeys subroutine uses the LoadSessionKeys SQL stored procedure. This procedure is included on the CD with the name CreateLoadSessionKeys.sql.

Building the Secure XML Web Service

The complete code for the XML Web service described in this section is contained in Listing 23.18. You can test the Login() method by opening the SecureService.asmx page in a Web browser. However, you cannot test the GetLuckyNumber() method because it requires the custom SOAP authentication header.

Listing 23.18 SecureService.asmx
<%@ WebService Class="SecureService" debug="True"%>
            Imports System
            Imports System.Web.Services
            Imports System.Web.Services.Protocols
            Imports System.Data
            Imports System.Data.SqlClient
            <WebService( Namespace:="http://yourdomain.com/webservices" )> _
            Public Class SecureService : Inherits WebService
            Public AuthenticationHeader As AuthHeader
            <WebMethod()> Public Function Login( username As String, password As String ) As ServiceTicket
            Dim conMyData As SqlConnection
            Dim cmdCheckPassword As SqlCommand
            Dim parmWork As SqlParameter
            Dim intUserID As Integer
            Dim intRole As Integer
            Dim objServiceTicket As ServiceTicket
            Dim drowSession As DataRow
            ' Initialize Sql command
            conMyData = New SqlConnection( "Server=localhost;UID=sa;pwd=secret; database=myData" )
            cmdCheckPassword = New SqlCommand( "CheckPassword", conMyData )
            cmdCheckPassword.CommandType = CommandType.StoredProcedure
            ' Add parameters
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@validuser", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.ReturnValue
            cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@username", username ) )
            cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@password", password ) )
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@sessionkey", SqlDbType.UniqueIdentifier ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@expiration", SqlDbType.DateTime ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@userID", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.Output
            parmWork = cmdCheckPassword.Parameters.Add( _
            New SqlParameter( "@role", SqlDbType.Int ) )
            parmWork.Direction = ParameterDirection.Output
            ' Execute the command
            conMyData.Open()
            cmdCheckPassword.ExecuteNonQuery()
            objServiceTicket = New ServiceTicket
            If cmdCheckPassword.Parameters( "@validuser" ).Value = 0 Then
            objServiceTicket.IsAuthenticated = True
            objServiceTicket.SessionKey = cmdCheckPassword.Parameters ( "@sessionkey" ).Value
            graphics/ccc.gif.ToString()
            objServiceTicket.Expiration = cmdCheckPassword.Parameters ( "@expiration" ).Value
            intUserID = cmdCheckPassword.Parameters( "@userID" ).Value
            intRole = cmdCheckPassword.Parameters( "@role" ).Value
            Else
            objServiceTicket.IsAuthenticated = False
            End If
            conMyData.Close()
            ' Add session to cache
            If objServiceTicket.IsAuthenticated Then
            If Context.Cache( "SessionKeys" ) Is Nothing Then
            LoadSessionKeys
            End If
            drowSession = Context.Cache( "SessionKeys" ).NewRow()
            drowSession( "session_key" ) = objServiceTicket.SessionKey
            drowSession( "session_expiration" ) = objServiceTicket.Expiration
            drowSession( "session_userID" ) = intUserID
            drowSession( "session_username" ) = username
            drowSession( "Session_role" ) = intRole
            Context.Cache( "SessionKeys" ).Rows.Add( drowSession )
            End If
            ' Return ServiceTicket
            Return objServiceTicket
            End Function
            <WebMethod(), SoapHeader( "AuthenticationHeader" )> _
            Public Function GetLuckyNumber As Integer
            If Authenticate( AuthenticationHeader ) Then
            Return 7
            End If
            End Function
            Private Sub LoadSessionKeys
            Dim conMyData As SqlConnection
            Dim dadMyData As SqlDataAdapter
            Dim dstSessionKeys As DataSet
            conMyData = New SqlConnection( "Server=localhost;UID=sa;PWD=secret; database=myData" )
            dadMyData = New SqlDataAdapter( "LoadSessionKeys", conMyData )
            dadMyData.SelectCommand.CommandType = CommandType.StoredProcedure
            dstSessionKeys = New DataSet
            dadMyData.Fill( dstSessionKeys, "SessionKeys" )
            Context.Cache.Insert( _
            "SessionKeys", _
            dstSessionKeys.Tables( "SessionKeys" ), _
            Nothing, _
            DateTime.Now.AddHours( 3 ), _
            TimeSpan.Zero )
            End Sub
            Private Function Authenticate( objAuthenticationHeader ) As Boolean
            Dim arrSessions As DataRow()
            Dim strMatch As String
            ' Load Session keys
            If Context.Cache( "SessionKeys" ) Is Nothing Then
            LoadSessionKeys
            End If
            ' Test for match
            strMatch = "session_key='" & objAuthenticationHeader.SessionKey
            strMatch &= "' And session_expiration > #" & DateTime.Now() & "#"
            arrSessions = Context.Cache( "SessionKeys" ).Select( strMatch )
            If arrSessions.Length > 0 Then
            Return True
            Else
            Return False
            End If
            End Function
            End Class
            Public Class AuthHeader:Inherits SoapHeader
            Public SessionKey As String
            End Class
            Public Class ServiceTicket
            Public IsAuthenticated As Boolean
            Public SessionKey As String
            Public Expiration As DateTime
            End Class
            

The C# version of this code can be found on the CD-ROM.

Accessing the Secure Web Service

Before you can access the SecureService Web service, you need to create a proxy class by executing the following two statements:


            
wsdl /l:vb /n:services http://localhost/webservices/SecureService.asmx?wsdl
            vbc /t:library /r:System.dll,System.Web.Services.dll,System.xml.dll SecureService.vb
            

After you create the proxy class, remember to copy it into your application /bin directory.

You can test the SecureService Web service by using the ASP.NET page in Listing 23.19. You'll need to change the username and password to match the username and password that you entered into the WebServiceUsers table.

Listing 23.19 TestSecureService.aspx
<%@ Import Namespace="Services" %>
            <Script runat="Server">
            Const strUsername As String = "Steve"
            Const strPassword As String = "Secret"
            Sub Page_Load
            Dim objSecureService As SecureService
            Dim objServiceTicket As ServiceTicket
            Dim objAuthHeader As AuthHeader
            objSecureService = New SecureService
            objServiceTicket = Session( "ServiceTicket" )
            ' Check for ticket existence
            If objServiceTicket Is Nothing Then
            objServiceTicket = objSecureService.Login( strUsername, strPassword )
            Session( "ServiceTicket" ) = objServiceTicket
            End If
            ' Check for ticket expiration
            If objServiceTicket.Expiration < DateTime.Now Then
            objServiceTicket = objSecureService.Login( strUsername, strPassword )
            Session( "ServiceTicket" ) = objServiceTicket
            End If
            ' Call the web service
            If objServiceTicket.IsAuthenticated Then
            objAuthHeader = New AuthHeader
            objAuthHeader.SessionKey = objServiceTicket.SessionKey
            objSecureService.AuthHeaderValue = objAuthHeader
            lblLuckyNumber.Text = objSecureService.GetLuckyNumber()
            Else
            lblLuckyNumber.Text = "Invalid username or password!"
            End If
            End Sub
            </Script>
            <html>
            <head><title>TestSecureService.aspx</title></head>
            <body>
            <asp:Label
            id="lblLuckyNumber"
            EnableViewState="False"
            Runat="Server" />
            </body>
            </html>
            

The C# version of this code can be found on the CD-ROM.

In the Page_Load subroutine in Listing 23.19, an instance of the SecureService proxy class is created. Next, a ServiceTicket is retrieved by calling the Login() Web service method. The ServiceTicket is stored in session state so that it can be used multiple times.

The subroutine checks whether the ServiceTicket has expired before trying to use it. If the Expiration property of the ServiceTicket class contains a time that has passed, the Login() method is called to retrieve a new ServiceTicket.

The ServiceTicket contains a session key in its SessionKey property. The session key is used when constructing the authentication header. The authentication header is created and the GetLuckyNumber() method is called with the following statements:


            
objAuthHeader = New AuthHeader
            objAuthHeader.SessionKey = objServiceTicket.SessionKey
            objSecureService.AuthHeaderValue = objAuthHeader
            lblLuckyNumber.Text = objSecureService.GetLuckyNumber()
            

The lucky number retrieved from the Web service is assigned to a label named lblLuckyNumber.

    [ Team LiB ] Previous Section Next Section