简体   繁体   中英

How to rewrite code to use IAuthorizationFilter with dependency injection instead of AuthorizeAttribute with service location in Asp Net Web Api?

I have the custom AuthorizeAttribute where I need to use one of the business layer services to validate some data in the database before giving user a permission to view the resource. In order to be able to allocate this service within the my AuthorizeAttribute I decided to use service location "anti-pattern", this is the code:

internal class AuthorizeGetGroupByIdAttribute : AuthorizeAttribute
{
    private readonly IUserGroupService _userGroupService;

    public AuthorizeGetGroupByIdAttribute()
    {
        _userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
    }

    //In this method I'm validating whether the user is a member of a group. 
    //If they are not they won't get a permission to view the resource, which is decorated with this attribute.
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
        int groupId = int.Parse(parameters["groupId"]);
        int currentUserId = HttpContext.Current.User.Identity.GetUserId();

        return _userGroupService.IsUserInGroup(currentUserId, groupId);
    }

    protected override void HandleUnauthorizedRequest(HttpActionContext actionContex)
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            base.HandleUnauthorizedRequest(actionContex);
        }
        else
        {
            actionContex.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
        }
    }
}

I have couple of other attributes like this in my application. Using service locator is probably not a good approach. After searching the web a little bit I found some people suggesting to use IAuthorizationFilter with dependency injection instead. But I don't know how to write this kind of IAuthorizationFilter . Can you help me writing IAuthorizationFilter that will do the same thing that the AuthorizeAttribute above?

So after struggling for a while I think I managed to resolve this issue. Here are the steps you have to do in order to that:

1) First you have to make GetGroupByIdAttribute passive, and by passive I mean an empty attribute without any logic within it (it will be used strictly for decoration purposes)

public class GetGroupByIdAttribute : Attribute
{
}

2) Then you have to mark a controller method, for which you want to add authorization, with this attribute.

[HttpPost]
[GetGroupById]
public IHttpActionResult GetGroupById(int groupId)
{
    //Some code
}

3) In order to write your own IAuthorizationFilter you have to implement its method ExecuteAuthorizationFilterAsync . Here is the full class (I included comments to guide you through the code):

public class GetGroupByIdAuthorizationFilter : IAuthorizationFilter
{
    public bool AllowMultiple { get; set; }

    private readonly IUserGroupService _userGroupService;

    //As you can see I'm using a constructor injection here
    public GetGroupByIdAuthorizationFilter(IUserGroupService userGroupService)
    {
        _userGroupService = userGroupService;
    }

    public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
    {
        //First I check whether the method is marked with the attribute, if it is then check whether the current user has a permission to use this method
        if (actionContext.ActionDescriptor.GetCustomAttributes<GetGroupByIdAttribute>().SingleOrDefault() != null)
        {
            Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
            int groupId = int.Parse(parameters["groupId"]);
            int currentUserId = HttpContext.Current.User.Identity.GetUserId();

            //If the user is not allowed to view view the resource, then return 403 status code forbidden
            if (!_userGroupService.IsUserInGroup(currentUserId, groupId))
            {
                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden));
            }
        }
        //If this line was reached it means the user is allowed to use this method, so just return continuation() which basically means continue processing 
        return continuation();
    }
}

4) The last step is to register your filter in the WebApiConfig .

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Here I am registering Dependency Resolver
        config.DependencyResolver = ServiceLocator.Instance.DependencyResolver;

        //Then I resolve the service I want to use (which should be fine because this is basically the start of the application)
        var userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();

        //And finally I'm registering the IAuthorizationFilter I created 
        config.Filters.Add(new GetGroupByIdAuthorizationFilter(userGroupService));

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );            
    }
}

Now, if needed, I can create additional IActionFilters that use IUserGroupService and then inject this service at the start of the application, from WebApiConfig class, into all filters.

Perhaps try it like shown here:

Add the following public method to your class.

public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
    // gets the dependecies from the serviceProvider 
    // and creates an instance of the filter
    return new GetGroupByIdAuthorizationFilter(
        (IUserGroupService )serviceProvider.GetService(typeof(IUserGroupService )));
}

Also Add interface IFilterMetadata to your class.

Now when your class is to be created the DI notices that there is a CreateInstance method and will use that rather then the constructor.

Alternatively you can get the interface directly from the DI in your method by calling

context.HttpContext.Features.Get<IUserGroupService>()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM