.NET Core 中的鉴权授权正确方式
1、在请求某个Action之前去做校验,验证当前操作者是否登录过,登录过就有权限
2、如果没有权限就跳转到登录页中去
3、传统登录授权用的AOP-Filter:ActionFilter。
具体实现为:
1、增加一个类CurrentUser.cs 保存用户登录信息
///
/// 登录用户的信息
///
public class CurrentUser
{
///
/// 用户Id
///
public int Id { get; set; }
///
/// 用户名称
///
public string Name { get; set; }
///
/// 账号
///
public string Account { get; set; }
}
2、建一个Cookice/Session帮助类CookieSessionHelper.cs
public static class CookieSessionHelper
{
public static void SetCookies(this HttpContext httpContext, string key, string value, int minutes = 30)
{
httpContext.Response.Cookies.Append(key, value, new CookieOptions
{
Expires = DateTime.Now.AddMinutes(minutes)
});
}
public static void DeleteCookies(this HttpContext httpContext, string key)
{
httpContext.Response.Cookies.Delete(key);
}
public static string GetCookiesValue(this HttpContext httpContext, string key)
{
httpContext.Request.Cookies.TryGetValue(key, out string value);
return value;
}
public static CurrentUser GetCurrentUserByCookie(this HttpContext httpContext)
{
httpContext.Request.Cookies.TryGetValue("CurrentUser", out string sUser);
if (sUser == null)
{
return null;
}
else
{
CurrentUser currentUser = Newtonsoft.Json.JsonConvert.DeserializeObject(sUser);
return currentUser;
}
}
public static CurrentUser GetCurrentUserBySession(this HttpContext context)
{
string sUser = context.Session.GetString("CurrentUser");
if (sUser == null)
{
return null;
}
else
{
CurrentUser currentUser = Newtonsoft.Json.JsonConvert.DeserializeObject(sUser);
return currentUser;
}
}
}
3、建一个登录控制器AccountController.cs
public class AccountController : Controller
{
//登录页面
public IActionResult Login()
{
return View();
}
//登录提交
[HttpPost]
public IActionResult LoginSub(IFormCollection fromData)
{
string userName = fromData["userName"].ToString();
string passWord = fromData["password"].ToString();
//真正写法是读数据库验证
if (userName == "test" && passWord == "123456")
{
#region 传统session/cookies
//登录成功,记录用户登录信息
CurrentUser currentUser = new CurrentUser()
{
Id = 123,
Name = "测试账号",
Account = userName
};
//写sessin
// HttpContext.Session.SetString("CurrentUser", JsonConvert.SerializeObject(currentUser));
//写cookies
HttpContext.SetCookies("CurrentUser", JsonConvert.SerializeObject(currentUser));
#endregion
//跳转到首页
return RedirectToAction("Index", "Home");
}
else
{
TempData["err"] = "账号或密码不正确";
//账号密码不对,跳回登录页
return RedirectToAction("Login", "Account");
}
}
///
/// 退出登录
///
///
public IActionResult LogOut()
{
HttpContext.DeleteCookies("CurrentUser");
//Session方式
// HttpContext.Session.Remove("CurrentUser");
return RedirectToAction("Login", "Account");
}
}
4、登录页Login.cshtml 内容
<form action="/Account/LoginSub" method="post">
<div>
账号:<input type="text" name="userName" />
div>
<div>
账号:<input type="password" name="passWord" />
div>
<div>
<input type="submit" value="登录" /> <span style="color:#ff0000">@TempData["err"]span>
div>
form>
5、建一个登录成功跳转到主页控制器HomeController.cs
public class HomeController : Controller
{
public IActionResult Index()
{
//从cookie获取用户信息
CurrentUser user = HttpContext.GetCurrentUserByCookie();
//CurrentUser user = HttpContext.GetCurrentUserBySession();
return View(user);
}
}
6、页面 Index.cshtml
@{
ViewData["Title"] = "Index";
}
@model SessionAuthorized.Demo.Models.CurrentUser
<h1>欢迎 @Model.Name 来到主页h1>
<div><a href="/Account/Logout">退出登录a>div>
7、增加鉴权过滤器MyActionAuthrizaFilterAttribute.cs,实现IActinFilter,在OnActionExecuting中写鉴权逻辑
public class MyActionAuthrizaFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
//throw new NotImplementedException();
}
///
/// 进入action前
///
///
public void OnActionExecuting(ActionExecutingContext context)
{
//throw new NotImplementedException();
Console.WriteLine("开始验证权限...");
// CurrentUser currentUser = context.HttpContext.GetCurrentUserBySession();
CurrentUser currentUser = context.HttpContext.GetCurrentUserByCookie();
if (currentUser == null)
{
Console.WriteLine("没有权限...");
if (this.IsAjaxRequest(context.HttpContext.Request))
{
context.Result = new JsonResult(new
{
Success = false,
Message = "没有权限"
});
}
context.Result = new RedirectResult("/Account/Login"); return;
}
Console.WriteLine("权限验证成功...");
}
private bool IsAjaxRequest(HttpRequest request)
{
string header = request.Headers["X-Requested-With"];
return "XMLHttpRequest".Equals(header);
}
}
在需要鉴权的控制器或方法上加上这个Filter即可完成鉴权,这里在主页中加入鉴权,登录成功的用户才能访问
8、如果要用Session,还要在startup.cs中加入Session
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSession();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
到这里,传统的鉴权就完成了,下面验证一下效果。
三、.NET5中正确的鉴权方式
传统的授权方式是通过Action Filter(before)来完成的,上图.Net Core的filter顺序可以发现,Action filter(befre)之前还有很多个filter,如果可以在前把鉴权做了,就能少跑了几步冤枉路,所以,正确的鉴权应该是在Authorization filter中做,Authorization filter是.NET5里面专门做鉴权授权用的。
怎么做呢,鉴权授权通过中间件支持。
1、在staup.cs的Configure方法里面的app.UseRouting();之后,在app.UseEndpoints()之前,增加鉴权授权;
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthentication();//检测用户是否登录
app.UseAuthorization(); //授权,检测有没有权限,是否能够访问功能
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
2、在ConfigureServices中增加
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//services.AddSession(); 传统鉴权
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.LoginPath = new PathString("/Account/Login");//没登录跳到这个路径
});
}
3、标记哪些控制器或方法需要登录认证,在控制器或方法头标记特性[Authorize],如果里面有方法不需要登录验证的,加上匿名访问标识 [AllowAnonymousAttribute]
// [MyActionAuthrizaFilterAttribute] 传统授权
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
//从cookie获取用户信息
// CurrentUser user = HttpContext.GetCurrentUserByCookie();
//CurrentUser user = HttpContext.GetCurrentUserBySession();
var userInfo = HttpContext.User;
CurrentUser user = new CurrentUser()
{
Id = Convert.ToInt32(userInfo.FindFirst("id").Value),
Name = userInfo.Identity.Name,
Account=userInfo.FindFirst("account").Value
};
return View(user);
}
///
/// 无需登录,匿名访问
///
///
[AllowAnonymousAttribute]
public IActionResult About()
{
return Content("欢迎来到关于页面");
}
}
4、登录处AccountController.cs的代码
public class AccountController : Controller
{
//登录页面
public IActionResult Login()
{
return View();
}
//登录提交
[HttpPost]
public IActionResult LoginSub(IFormCollection fromData)
{
string userName = fromData["userName"].ToString();
string passWord = fromData["password"].ToString();
//真正写法是读数据库验证
if (userName == "test" && passWord == "123456")
{
#region 传统session/cookies
//登录成功,记录用户登录信息
//CurrentUser currentUser = new CurrentUser()
//{
// Id = 123,
// Name = "测试账号",
// Account = userName
//};
//写sessin
// HttpContext.Session.SetString("CurrentUser", JsonConvert.SerializeObject(currentUser));
//写cookies
//HttpContext.SetCookies("CurrentUser", JsonConvert.SerializeObject(currentUser));
#endregion
//用户角色列表,实际操作是读数据库
var roleList = new List<string>()
{
"Admin",
"Test"
};
var claims = new List() //用Claim保存用户信息
{
new Claim(ClaimTypes.Name,"测试账号"),
new Claim("id","1"),
new Claim("account",userName),//...可以增加任意信息
};
//填充角色
foreach(var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
//把用户信息装到ClaimsPrincipal
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Customer"));
//登录,把用户信息写入到cookie
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.Now.AddMinutes(30)//过期时间30分钟
}).Wait();
//跳转到首页
return RedirectToAction("Index", "Home");
}
else
{
TempData["err"] = "账号或密码不正确";
//账号密码不对,跳回登录页
return RedirectToAction("Login", "Account");
}
}
///
/// 退出登录
///
///
public IActionResult LogOut()
{
// HttpContext.DeleteCookies("CurrentUser");
//Session方式
// HttpContext.Session.Remove("CurrentUser");
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Login", "Account");
}
}
5、验证结果:
可以看到,一开始没登录状态,访问/Home/Index会跳转到登录页面,访问/Home/About能成功访问,证明匿名访问ok,后面的登录,显示用户信息,退出登录也没问题,证明功能没问题,鉴权到这里就完成了。
四、.NET5中角色授权
上面的claims中已经记录了用户角色,这个角色就可以用来做授权了。
在startup.cs中修改没权限时跳转页面路径
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//services.AddSession(); 传统鉴权
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.LoginPath = new PathString("/Account/Login");//没登录跳到这个路径
options.AccessDeniedPath = new PathString("/Account/AccessDenied");//没权限跳到这个路径
});
}
AccountController.cs增加方法
public IActionResult AccessDenied()
{
return View();
}
视图内容
没有权限访问-401
1、单个角色访问权限
在方法头加上特性 [Authorize(Roles ="角色代码")]
在HomeController.cs中增加一个方法
///
/// 角色为Admin能访问
///
///
[Authorize(Roles ="Admin")]
public IActionResult roleData1() {
return Content("Admin能访问");
}
验证。
开始角色为
访问roleData1数据:
访问成功,然后把角色Admin去掉
var roleList = new List<string>(){ //"Admin","Test"};
重新登录,在访问rleData1数据:
访问不成功,跳转到预设的没权限的页面了。
2、多个角色包含一个权限
[Authorize(Roles = "Admin,Test")]//多个角色用逗号隔开,角色包含有其中一个就能访问
public IActionResult roleData2()
{
return Content("roleData2访问成功");
}
3、多个角色组合权限
///
/// 同时拥有标记的全部角色才能访问
///
///
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Test")]
public IActionResult roleData3()
{
return Content("roleData3访问成功");
}
五、自定义策略授权
上面的角色授权的缺点在哪里呢,最大的缺点就是角色要提前写死到方法上,如果要修改只能改代码,明显很麻烦,实际项目中权限都是根据配置修改的,
所以就要用到自定义策略授权了。
第一步:
增加一个CustomAuthorizatinRequirement.cs,要求实现接口:IAuthorizationRequirement
///
/// 策略授权参数
///
public class CustomAuthorizationRequirement: IAuthorizationRequirement
{
///
///
///
public CustomAuthorizationRequirement(string policyname)
{
this.Name = policyname;
}
public string Name { get; set; }
}
增加CustomAuthorizationHandler.cs------专门做检验逻辑的;要求继承自AuthorizationHandler<>泛型抽象类;
///
/// 自定义授权策略
///
public class CustomAuthorizationHandler: AuthorizationHandler<CustomAuthorizationRequirement>
{
public CustomAuthorizationHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)
{
bool flag = false;
if (requirement.Name == "Policy01")
{
Console.WriteLine("进入自定义策略授权01...");
///策略1的逻辑
}
if (requirement.Name == "Policy02")
{
Console.WriteLine("进入自定义策略授权02...");
///策略2的逻辑
}
if(flag)
{
context.Succeed(requirement); //验证通过了
}
return Task.CompletedTask; //验证不同过
}
}
第二步,让自定义的逻辑生效。
starup.cs的ConfigureServices方法中注册进来
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//services.AddSession(); 传统鉴权
services.AddSingleton();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/Login");//没登录跳到这个路径
options.AccessDeniedPath = new PathString("/Account/AccessDenied");//没权限跳到这个路径
});
services.AddAuthorization(optins =>
{
//增加授权策略
optins.AddPolicy("customPolicy", polic =>
{
polic.AddRequirements(new CustomAuthorizationRequirement("Policy01")
// ,new CustomAuthorizationRequirement("Policy02")
);
});
});
}
第三步,把要进授权策略的控制器或方法增加标识
HomeContrller.cs增加测试方法
///
/// 进入授权策略
///
///
[Authorize(policy: "customPolicy")]
public IActionResult roleData4()
{
return Content("自定义授权策略");
}
访问roleData4,看是否进到自定义授权策略逻辑
可以看到自定义授权策略生效了,授权策略就可以在这里做了,下面加上授权逻辑。
我这里的权限用路径和角色关联授权,加上授权逻辑后的校验代码。
///
/// 自定义授权策略
///
public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement>
{
public CustomAuthorizationHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)
{
bool flag = false;
//把context转换到httpConext,方便取上下文
HttpContext httpContext = context.Resource as HttpContext;
string path = httpContext.Request.Path;//当前访问路径,例:"/Home/roleData4"
var user = httpContext.User;
//用户id
string userId = user.FindFirst("id")?.Value;
if (userId == null)
{
//没登录,直接结束
return Task.CompletedTask;
}
//登录成功时根据角色查出来这个用户的权限存到redis,这里实际是根据用户id从redis查询出来
List<string> paths = new List<string>()
{
"/Home/roleData4",
"/Home/roleData3"
};
if (requirement.Name == "Policy01")
{
Console.WriteLine("进入自定义策略授权01...");
///策略1的逻辑
if (paths.Contains(path))
{
flag = true;
}
}
if (requirement.Name == "Policy02")
{
Console.WriteLine("进入自定义策略授权02...");
///策略2的逻辑
}
if (flag)
{
context.Succeed(requirement); //验证通过了
}
return Task.CompletedTask; //验证不同过
}
}
加上逻辑后再访问。
访问成功,自定义授权策略完成。
源代码:https://github.com/weixiaolong325/SessionAuthorized.Demo
转自:包子wxl
链接:cnblogs.com/wei325/p/15575141.html