Tại sao AuthorizeAttribution chuyển hướng đến trang đăng nhập để xác thực và lỗi ủy quyền?


265

Trong ASP.NET MVC, bạn có thể đánh dấu một phương thức điều khiển bằng AuthorizeAttribute, như sau:

[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

Điều này có nghĩa là, nếu người dùng hiện đang đăng nhập không có vai trò "CanDeleteTags", phương thức điều khiển sẽ không bao giờ được gọi.

Thật không may, đối với các lỗi, AuthorizeAttributetrả về HttpUnauthorizedResult, luôn trả về mã trạng thái HTTP 401. Điều này gây ra chuyển hướng đến trang đăng nhập.

Nếu người dùng không đăng nhập, điều này có ý nghĩa hoàn hảo. Tuy nhiên, nếu người dùng đã đăng nhập nhưng không ở vai trò bắt buộc, thật khó hiểu khi gửi họ trở lại trang đăng nhập.

Có vẻ như AuthorizeAttributexác nhận và ủy quyền.

Điều này có vẻ như một chút giám sát trong ASP.NET MVC, hoặc tôi đang thiếu một cái gì đó?

Tôi đã phải nấu một DemandRoleAttributecái tách hai cái đó ra. Khi người dùng không được xác thực, nó sẽ trả về HTTP 401, gửi chúng đến trang đăng nhập. Khi người dùng đăng nhập, nhưng không ở vai trò bắt buộc, nó sẽ tạo một NotAuthorizedResultthay thế. Hiện tại điều này chuyển hướng đến một trang lỗi.

Chắc chắn tôi không phải làm điều này?


10
Câu hỏi tuyệt vời và tôi đồng ý, nó sẽ ném trạng thái Không được ủy quyền HTTP.
Pure.Krom

3
Tôi thích giải pháp của bạn, Roger. Ngay cả khi bạn không.
Jon Davis

Trang Đăng nhập của tôi có một kiểm tra chỉ đơn giản là chuyển hướng người dùng đến ReturnUrl, nếu họ đã được tự động. Vì vậy, tôi đã quản lý để tạo ra một vòng lặp vô hạn gồm 302 chuyển hướng: D woot.
juhan_h

1
Kiểm tra này .
Jogi

Roger, bài viết hay về giải pháp của bạn - red-gate.com/simple-talk/dotnet/asp-net/NH Có vẻ như giải pháp của bạn là cách duy nhất để thực hiện việc này một cách sạch sẽ
Craig

Câu trả lời:


305

Khi được phát triển lần đầu tiên, System.Web.Mvc.AuthorizeAttribution đã làm đúng - các phiên bản cũ hơn của đặc tả HTTP đã sử dụng mã trạng thái 401 cho cả "không được phép" và "không được xác thực".

Từ đặc điểm kỹ thuật ban đầu:

Nếu yêu cầu đã bao gồm thông tin xác thực ủy quyền, thì phản hồi 401 cho biết rằng ủy quyền đã bị từ chối cho các thông tin đăng nhập đó.

Trên thực tế, bạn có thể thấy sự nhầm lẫn ngay tại đó - nó sử dụng từ "ủy quyền" khi nó có nghĩa là "xác thực". Tuy nhiên, trong thực tế hàng ngày, việc trả lại 403 Cấm khi người dùng được xác thực nhưng không được ủy quyền sẽ có ý nghĩa hơn. Người dùng không có khả năng sẽ có một bộ thông tin xác thực thứ hai sẽ cung cấp cho họ quyền truy cập - trải nghiệm người dùng xấu xung quanh.

Hãy xem xét hầu hết các hệ điều hành - khi bạn cố đọc một tệp mà bạn không có quyền truy cập, bạn sẽ không hiển thị màn hình đăng nhập!

Rất may, các thông số kỹ thuật HTTP đã được cập nhật (tháng 6 năm 2014) để loại bỏ sự mơ hồ.

Từ "Giao thức truyền tải siêu văn bản (HTTP / 1.1): Xác thực" (RFC 7235):

Mã trạng thái 401 (Không được phép) cho biết rằng yêu cầu chưa được áp dụng vì nó thiếu thông tin xác thực hợp lệ cho tài nguyên đích.

Từ "Giao thức truyền siêu văn bản (HTTP / 1.1): Ngữ nghĩa và nội dung" (RFC 7231):

Mã trạng thái 403 (Bị cấm) chỉ ra rằng máy chủ hiểu yêu cầu nhưng từ chối ủy quyền.

Thật thú vị, tại thời điểm ASP.NET MVC 1 được phát hành, hành vi của AuthorizeAttribution là chính xác. Bây giờ, hành vi không chính xác - đặc tả HTTP / 1.1 đã được sửa.

Thay vì cố gắng thay đổi chuyển hướng trang đăng nhập của ASP.NET, việc khắc phục sự cố tại nguồn dễ dàng hơn. Bạn có thể tạo một thuộc tính mới có cùng tên ( AuthorizeAttribute) trong không gian tên mặc định của trang web của bạn (điều này rất quan trọng), sau đó trình biên dịch sẽ tự động chọn nó thay vì tiêu chuẩn của MVC. Tất nhiên, bạn luôn có thể đặt cho thuộc tính một tên mới nếu bạn muốn sử dụng phương pháp đó.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

52
+1 Cách tiếp cận rất tốt. Một gợi ý nhỏ: thay vì kiểm tra filterContext.HttpContext.User.Identity.IsAuthenticated, bạn chỉ có thể kiểm tra filterContext.HttpContext.Request.IsAuthenticated, đi kèm với kiểm tra null được tích hợp. Xem stackoverflow.com/questions/1379566/ Kẻ
Daniel Liuzzi

> Bạn có thể tạo một thuộc tính mới có cùng tên (AuthorizeAttribution) trong không gian tên mặc định của trang web của bạn, sau đó trình biên dịch sẽ tự động chọn nó thay vì tiêu chuẩn của MVC. Điều này dẫn đến một lỗi: Không thể tìm thấy loại hoặc không gian tên 'Ủy quyền' (bạn có thiếu chỉ thị hoặc tham chiếu lắp ráp không?) Cả hai đều sử dụng System.Web.Mvc; và không gian tên cho lớp AuthorizeAttribution tùy chỉnh của tôi được tham chiếu trong bộ điều khiển. Để giải quyết vấn đề này, tôi đã phải sử dụng [MyNamepace.Authorize]
cơn bão

2
@DePeter thông số kỹ thuật không bao giờ nói bất cứ điều gì về chuyển hướng, vậy tại sao chuyển hướng là một giải pháp tốt hơn? Điều này một mình giết chết các yêu cầu ajax mà không cần hack để giải quyết nó.
Adam Tuliper - MSFT

1
Điều đó nên được đăng nhập trên MS Connect vì đây rõ ràng là một lỗi hành vi. Cảm ơn.
Tony Wall

BTW, tại sao chúng tôi được chuyển hướng đến trang đăng nhập? Tại sao không chỉ xuất mã 401 và trang đăng nhập trực tiếp trong cùng một yêu cầu?
SandRock

25

Thêm phần này vào chức năng Page_Load đăng nhập của bạn:

// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

Khi người dùng được chuyển hướng đến đó nhưng đã đăng nhập, nó sẽ hiển thị trang trái phép. Nếu họ không đăng nhập, nó sẽ rơi vào và hiển thị trang đăng nhập.


18
Page_Load là một mojo dạng web
Chance

2
@Chance - sau đó thực hiện điều đó trong ActionMethod mặc định cho bộ điều khiển được gọi là nơi FormsAuthencation đã được thiết lập để gọi.
Pure.Krom

Điều này thực sự hoạt động thực sự tốt mặc dù đối với MVC, nó phải giống như tên trái phépif (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized"); trong đó tên tuyến được xác định.
Moses Machua

Vì vậy, bạn hỏi một nguồn lực, bạn được chuyển hướng đến một trang đăng nhập và bạn được chuyển một lần nữa đến một trang 403? Có vẻ xấu với tôi. Tôi thậm chí không thể chịu đựng được một chuyển hướng nào cả. IMO điều này được xây dựng rất xấu.
SandRock

3
Theo giải pháp của bạn, nếu bạn đã đăng nhập và vào trang Đăng nhập bằng cách nhập URL ... điều này sẽ đưa bạn đến trang Không được phép. Điều đó không đúng.
Rajshekar Reddy

4

Tôi luôn nghĩ rằng điều này có ý nghĩa. Nếu bạn đã đăng nhập và bạn cố gắng truy cập một trang yêu cầu vai trò mà bạn không có, bạn sẽ được chuyển tiếp đến màn hình đăng nhập yêu cầu bạn đăng nhập với người dùng có vai trò đó.

Bạn có thể thêm logic vào trang đăng nhập để kiểm tra xem người dùng đã được xác thực chưa. Bạn có thể thêm một tin nhắn thân thiện giải thích lý do tại sao họ lại bị đánh bại ở đó một lần nữa.


4
Tôi cảm thấy rằng hầu hết mọi người không có xu hướng có nhiều hơn một danh tính cho một ứng dụng web nhất định. Nếu họ làm như vậy, thì họ đủ thông minh để nghĩ rằng "ID hiện tại của tôi không có mojo, tôi sẽ đăng nhập lại như một cái khác".
Roger Lipscombe

Mặc dù điểm khác của bạn về việc hiển thị một cái gì đó trên trang đăng nhập là một điểm tốt. Cảm ơn.
Roger Lipscombe

4

Thật không may, bạn đang xử lý hành vi mặc định của xác thực mẫu ASP.NET. Có một cách giải quyết (tôi chưa thử) đã thảo luận ở đây:

http://www.codeproject.com/KB/aspnet/Custon401Page.aspx

(Nó không dành riêng cho MVC)

Tôi nghĩ rằng trong hầu hết các trường hợp, giải pháp tốt nhất là hạn chế quyền truy cập vào các tài nguyên trái phép trước khi người dùng cố gắng đến đó. Bằng cách xóa / tô màu liên kết hoặc nút có thể đưa họ đến trang trái phép này.

Có lẽ sẽ rất tốt nếu có một tham số bổ sung trên thuộc tính để chỉ định nơi chuyển hướng người dùng trái phép. Nhưng trong lúc này, tôi xem AuthorizeAttribution như một mạng lưới an toàn.


Tôi cũng có kế hoạch xóa liên kết dựa trên ủy quyền (tôi đã thấy một câu hỏi ở đây về vấn đề đó ở đâu đó), vì vậy tôi sẽ mã hóa một phương thức mở rộng HtmlHelper sau này.
Roger Lipscombe

1
Tôi vẫn phải ngăn người dùng truy cập trực tiếp vào URL, đó là tất cả những gì thuộc tính này. Tôi không quá hài lòng với giải pháp Custom 401 (có vẻ hơi toàn cầu), vì vậy tôi sẽ thử mô hình hóa NotAuthorizedResult của mình trên RedirectToRouteResult ...
Roger Lipscombe

0

Hãy thử điều này trong trình xử lý Application_EndRequest của tệp Global.ascx của bạn

if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}

0

Nếu bạn sử dụng aspnetcore 2.0, hãy sử dụng:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}

0

Trong trường hợp của tôi, vấn đề là "đặc tả HTTP đã sử dụng mã trạng thái 401 cho cả" không được ủy quyền "và" không được xác thực "". Như ShadowChaser đã nói.

Giải pháp này hiệu quả với tôi:

if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.