зеркало из https://github.com/aspnet/Mvc.git
[Fixes #911] RequireHttpsAttribute does not exist in MVC 6
1. GET requests will be redirected to the equivalent HTTPS url. 2. Requests with any other http method will fail with a 400.
This commit is contained in:
Родитель
18400481b5
Коммит
25838cee55
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
|
||||
public class RequireHttpsAttribute :
|
||||
Attribute, IAuthorizationFilter, IOrderedFilter
|
||||
{
|
||||
public int Order { get; set; }
|
||||
|
||||
public virtual void OnAuthorization([NotNull]AuthorizationContext filterContext)
|
||||
{
|
||||
if (!filterContext.HttpContext.Request.IsSecure)
|
||||
{
|
||||
HandleNonHttpsRequest(filterContext);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
|
||||
{
|
||||
// only redirect for GET requests, otherwise the browser might not propagate the verb and request
|
||||
// body correctly.
|
||||
if (!string.Equals(filterContext.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filterContext.Result = new HttpStatusCodeResult(403);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = filterContext.HttpContext.Request;
|
||||
var newUrl = string.Concat(
|
||||
"https://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent(),
|
||||
request.QueryString.ToUriComponent());
|
||||
|
||||
// redirect to HTTPS version of page
|
||||
filterContext.Result = new RedirectResult(newUrl, permanent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Microsoft.AspNet.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
||||
public class RequireHttpsAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnAuthorization_AllowsTheRequestIfItIsSecure()
|
||||
{
|
||||
// Arrange
|
||||
var requestContext = new DefaultHttpContext();
|
||||
requestContext.Request.Scheme = "https";
|
||||
|
||||
var authContext = CreateAuthorizationContext(requestContext);
|
||||
var attr = new RequireHttpsAttribute();
|
||||
|
||||
// Act
|
||||
attr.OnAuthorization(authContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(authContext.Result);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> RedirectToHttpEndpointTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
// host, pathbase, path, query, expectedRedirectUrl
|
||||
var data = new TheoryData<string, string, string, string, string>();
|
||||
|
||||
data.Add("localhost", null, null, null, "https://localhost");
|
||||
data.Add("localhost:5000", null, null, null, "https://localhost:5000");
|
||||
data.Add("localhost", "/pathbase", null, null, "https://localhost/pathbase");
|
||||
data.Add("localhost", "/pathbase", "/path", null, "https://localhost/pathbase/path");
|
||||
data.Add("localhost", "/pathbase", "/path", "?foo=bar", "https://localhost/pathbase/path?foo=bar");
|
||||
|
||||
// Encode some special characters on the url.
|
||||
data.Add("localhost", "/path?base", null, null, "https://localhost/path%3Fbase");
|
||||
data.Add("localhost", null, "/pa?th", null, "https://localhost/pa%3Fth");
|
||||
data.Add("localhost", "/", null, "?foo=bar%2Fbaz", "https://localhost/?foo=bar%2Fbaz");
|
||||
|
||||
// Urls with punycode
|
||||
// 本地主機 is "localhost" in chinese traditional, "xn--tiq21tzznx7c" is the
|
||||
// punycode representation.
|
||||
data.Add("本地主機", "/", null, null, "https://xn--tiq21tzznx7c/");
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RedirectToHttpEndpointTestData))]
|
||||
public void OnAuthorization_RedirectsToHttpsEndpoint_ForNonHttpsGetRequests(
|
||||
string host,
|
||||
string pathBase,
|
||||
string path,
|
||||
string queryString,
|
||||
string expectedUrl)
|
||||
{
|
||||
// Arrange
|
||||
var requestContext = new DefaultHttpContext();
|
||||
requestContext.Request.Scheme = "http";
|
||||
requestContext.Request.Method = "GET";
|
||||
requestContext.Request.Host = HostString.FromUriComponent(host);
|
||||
|
||||
if (pathBase != null)
|
||||
{
|
||||
requestContext.Request.PathBase = new PathString(pathBase);
|
||||
}
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
requestContext.Request.Path = new PathString(path);
|
||||
}
|
||||
|
||||
if (queryString != null)
|
||||
{
|
||||
requestContext.Request.QueryString = new QueryString(queryString);
|
||||
}
|
||||
|
||||
var authContext = CreateAuthorizationContext(requestContext);
|
||||
var attr = new RequireHttpsAttribute();
|
||||
|
||||
// Act
|
||||
attr.OnAuthorization(authContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(authContext.Result);
|
||||
var result = Assert.IsType<RedirectResult>(authContext.Result);
|
||||
|
||||
Assert.True(result.Permanent);
|
||||
Assert.Equal(expectedUrl, result.Url);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("POST")]
|
||||
[InlineData("PUT")]
|
||||
[InlineData("PATCH")]
|
||||
[InlineData("DELETE")]
|
||||
public void OnAuthorization_SignalsBadRequestStatusCode_ForNonHttpsAndNonGetRequests(string method)
|
||||
{
|
||||
// Arrange
|
||||
var requestContext = new DefaultHttpContext();
|
||||
requestContext.Request.Scheme = "http";
|
||||
requestContext.Request.Method = method;
|
||||
var authContext = CreateAuthorizationContext(requestContext);
|
||||
var attr = new RequireHttpsAttribute();
|
||||
|
||||
// Act
|
||||
attr.OnAuthorization(authContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(authContext.Result);
|
||||
var result = Assert.IsType<HttpStatusCodeResult>(authContext.Result);
|
||||
Assert.Equal(403, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleNonHttpsRequestExtensibility()
|
||||
{
|
||||
// Arrange
|
||||
var requestContext = new DefaultHttpContext();
|
||||
requestContext.Request.Scheme = "http";
|
||||
|
||||
var authContext = CreateAuthorizationContext(requestContext);
|
||||
var attr = new CustomRequireHttpsAttribute();
|
||||
|
||||
// Act
|
||||
attr.OnAuthorization(authContext);
|
||||
|
||||
// Assert
|
||||
var result = Assert.IsType<HttpStatusCodeResult>(authContext.Result);
|
||||
Assert.Equal(404, result.StatusCode);
|
||||
}
|
||||
|
||||
private class CustomRequireHttpsAttribute : RequireHttpsAttribute
|
||||
{
|
||||
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
|
||||
{
|
||||
filterContext.Result = new HttpStatusCodeResult(404);
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthorizationContext CreateAuthorizationContext(HttpContext ctx)
|
||||
{
|
||||
var actionContext = new ActionContext(ctx, new RouteData(), actionDescriptor: null);
|
||||
|
||||
return new AuthorizationContext(actionContext, Enumerable.Empty<IFilter>().ToList());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -125,5 +126,63 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Equal(expectedContent, responseContent);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActionWithRequireHttps_RedirectsToSecureUrl_ForNonHttpsGetRequests()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Home/HttpsOnlyAction");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode);
|
||||
Assert.NotNull(response.Headers.Location);
|
||||
Assert.Equal("https://localhost/Home/HttpsOnlyAction", response.Headers.Location.ToString());
|
||||
Assert.Equal(0, response.Content.Headers.ContentLength);
|
||||
|
||||
var responseBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
Assert.Equal(0, responseBytes.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActionWithRequireHttps_ReturnsBadRequestResponse_ForNonHttpsNonGetRequests()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"http://localhost/Home/HttpsOnlyAction"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
Assert.Equal(0, response.Content.Headers.ContentLength);
|
||||
|
||||
var responseBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
Assert.Equal(0, responseBytes.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("POST")]
|
||||
public async Task ActionWithRequireHttps_AllowsHttpsRequests(string method)
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = new HttpClient(server.CreateHandler(), false);
|
||||
|
||||
// Act
|
||||
var response = await client.SendAsync(new HttpRequestMessage(
|
||||
new HttpMethod(method),
|
||||
"https://localhost/Home/HttpsOnlyAction"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,13 @@ namespace BasicWebSite.Controllers
|
|||
return new HttpStatusCodeResult(204);
|
||||
}
|
||||
|
||||
[AcceptVerbs("GET", "POST")]
|
||||
[RequireHttps]
|
||||
public IActionResult HttpsOnlyAction()
|
||||
{
|
||||
return new HttpStatusCodeResult(200);
|
||||
}
|
||||
|
||||
public async Task ActionReturningTask()
|
||||
{
|
||||
// TODO: #1077. With HttpResponseMessage, there seems to be a race between the write operation setting the
|
||||
|
|
Загрузка…
Ссылка в новой задаче