本章讨论创建安全的WebApi服务,到目前为止,我们实现的API都是基于未加密的HTTP协议,大家都知道在Web中传递身份信息必须通过HTTPS,接下来我们来实现这一过程。
使用HTTPS
其实可以通过IIS配置,将整个WebApi的访问都配置为Https,但实际上,如果希望只是对部分方法进行认证,那就必须通过认证身份信息进行处理。
下面介绍通过Filter来实现这一过程,如果身份认证不通过,就返回一条信息,提示访问者通过https进行访问。
1: public class ForceHttpsAttribute : AuthorizationFilterAttribute
2: {
3: public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
4: {
5: var request = actionContext.Request;
6:
7: if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
8: {
9: var html = "<p>Https is required</p>";
10:
11: if (request.Method.Method == "GET")
12: {
13: actionContext.Response = request.CreateResponse(HttpStatusCode.Found);
14: actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
15:
16: UriBuilder httpsNewUri = new UriBuilder(request.RequestUri);
17: httpsNewUri.Scheme = Uri.UriSchemeHttps;
18: httpsNewUri.Port = 443;
19:
20: actionContext.Response.Headers.Location = httpsNewUri.Uri;
21: }
22: else
23: {
24: actionContext.Response = request.CreateResponse(HttpStatusCode.NotFound);
25: actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
26: }
27:
28: }
29: }
30: }
通过actionContext参数获取Request和Response对象,对URI进行检查,如果不是以HTTPS开头,就返回443代码。
使用的方法有两种,一种是在WebAPIConfig中注册为全局的Attribute。
1: public static void Register(HttpConfiguration config)
2: {
3: config.Filters.Add(new ForceHttpsAttribute());
4: }
另一种是对制定的类或者方法进行拦截。
1: //Enforce HTTPS on the entire controller
2: [Learning.Web.Filters.ForceHttps()]
3: public class CoursesController : BaseApiController
4: {
5: //Enforce HTTPS on POST method only
6: [Learning.Web.Filters.ForceHttps()]
7: public HttpResponseMessage Post([FromBody] CourseModel courseModel)
8: {
9:
10: }
11: }
通过Basic Authentication进行认证
当前所有的API都是Public的,网络上的任意用户都可以请求资源。实际的项目中肯定要对访问者进行必要的限制。
- 假设对于客户端的请求“http://{your_port}/api/students/{userName}”,当有正确的身份信息时,我们返回username为 “TaiseerJoudeh”的信息,否则,返回错误;
- 如果请求是POST类型,如“http://{your_port}/api/courses/2/students/{userName}”,那么修改者也必须通过身份认证才可以进行修改。
什么是Basic Authentication
Basic Authentication提供了一种在Http Request被处理之前先行进行身份认证的模式,它在防止Dos等方面具有重要作用。Basic Authentication要求在请求时必须在Http Header提供基于Base64编码的用户名和密码信息。这种认证一般应该通过HTTPS来实现。
1: public class LearningAuthorizeAttribute : AuthorizationFilterAttribute
2: {
3:
4: [Inject]
5: public LearningRepository TheRepository { get; set; }
6:
7: public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
8: {
9: //Case that user is authenticated using forms authentication
10: //so no need to check header for basic authentication.
11: if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
12: {
13: return;
14: }
15:
16: var authHeader = actionContext.Request.Headers.Authorization;
17:
18: if (authHeader != null)
19: {
20: if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
21: !String.IsNullOrWhiteSpace(authHeader.Parameter))
22: {
23: var credArray = GetCredentials(authHeader);
24: var userName = credArray[0];
25: var password = credArray[1];
26:
27: if (IsResourceOwner(userName, actionContext))
28: {
29: //You can use Websecurity or asp.net memebrship provider to login, for
30: //for he sake of keeping example simple, we used out own login functionality
31: if (TheRepository.LoginStudent(userName, password))
32: {
33: var currentPrincipal = new GenericPrincipal(new GenericIdentity(userName), null);
34: Thread.CurrentPrincipal = currentPrincipal;
35: return;
36: }
37: }
38: }
39: }
40:
41: HandleUnauthorizedRequest(actionContext);
42: }
43:
44: private string[] GetCredentials(System.Net.Http.Headers.AuthenticationHeaderValue authHeader)
45: {
46:
47: //Base 64 encoded string
48: var rawCred = authHeader.Parameter;
49: var encoding = Encoding.GetEncoding("iso-8859-1");
50: var cred = encoding.GetString(Convert.FromBase64String(rawCred));
51:
52: var credArray = cred.Split(‘:‘);
53:
54: return credArray;
55: }
56:
57: private bool IsResourceOwner(string userName, System.Web.Http.Controllers.HttpActionContext actionContext)
58: {
59: var routeData = actionContext.Request.GetRouteData();
60: var resourceUserName = routeData.Values["userName"] as string;
61:
62: if (resourceUserName == userName)
63: {
64: return true;
65: }
66: return false;
67: }
68:
69: private void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
70: {
71: actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
72:
73: actionContext.Response.Headers.Add("WWW-Authenticate",
74: "Basic Scheme=‘eLearning‘ location=‘http://localhost:8323/account/login‘");
75:
76: }
77: }
上述代码实现了以下逻辑:
- 从Request Header中获取认证信息;
- 确认Header的Schema被设置为basic,并包含了正确的Base64编码字符串;
- 将字符串转换为“username:password”格式,获取各自内容;
- 验证身份信息,确定是否具有相应权限;
- 如果Credentials验证无误,设置当前线程的Identity信息,以便子请求能够重用;
- 如果验证有无,则返回401错误。
现在可以对指定的方法进行标记。
1: public class StudentsController : BaseApiController
2: {
3: [LearningAuthorizeAttribute]
4: public HttpResponseMessage Get(string userName)
5: {
6:
7: }
8: }
9:
10: public class EnrollmentsController : BaseApiController
11: {
12: [LearningAuthorizeAttribute]
13: public HttpResponseMessage Post(int courseId, [FromUri]string userName, [FromBody]Enrollment enrollment)
14: {
15:
16: }
17: }
现在分别用FireFox和Fiddler进行测试。假设路径如下:http://localhost:{your_port}/api/students/TaiseerJoudeh。-
FireFox:返回401,因为没有身份信息,一个弹出框会弹出,要求输入用户名和密码。输入正确的用户名和密码,就可以看到返回的json信息。而接下来的子请求,则无需再进行认证;
-
Fiddler:需要先拼接一个Base64加密“username:password”字符串,可以通过这里实现。注意:这不能算是加密,真正的加密必须通过HTTPS实现。当用户名密码正确时,会返回200和正确的json信息。