一、简介

前后端分离的站点一般都会用jwt或IdentityServer4之类的生成token的方式进行登录鉴权。这里要说的是小项目没有做前后端分离的时站点登录授权的正确方式。

一、传统的授权方式

这里说一下传统授权方式,传统授权方式用session或cookies来完成。

1.在请求某个Action之前去做校验,验证当前操作者是否登录过,登录过就有权限

2.如果没有权限就跳转到登录页中去

3.传统登录授权用的AOP-Filter:ActionFilter。

具体实现为:

1.增加一个类CurrentUser.cs 保存用户登录信息

 /// <summary>
/// 登录用户的信息
/// </summary>
public class CurrentUser
{
/// <summary>
/// 用户Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 账号
/// </summary>
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<CurrentUser>(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<CurrentUser>(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");
}
}
/// <summary>
/// 退出登录
/// </summary>
/// <returns></returns>
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();
}
/// <summary>
/// 进入action前
/// </summary>
/// <param name="context"></param>
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);
} /// <summary>
/// 无需登录,匿名访问
/// </summary>
/// <returns></returns>
[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>() //用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");
}
}
/// <summary>
/// 退出登录
/// </summary>
/// <returns></returns>
public IActionResult LogOut()
{
// HttpContext.DeleteCookies("CurrentUser");
//Session方式
// HttpContext.Session.Remove("CurrentUser"); HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Login", "Account");
}
}

5.验证结果:

可以看到,一开始没登录状态,访问/Home/Index会跳转到登录页面,访问/Home/Index能成功访问,证明匿名访问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中增加一个方法

     /// <summary>
/// 角色为Admin能访问
/// </summary>
/// <returns></returns>
[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.“多个角色组合”权限

     /// <summary>
/// 同时拥有标记的全部角色才能访问
/// </summary>
/// <returns></returns>
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Test")]
public IActionResult roleData3()
{
return Content("roleData3访问成功");
}

三、自定义策略授权

上面的角色授权的缺点在哪里呢,最大的缺点就是角色要提前写死到方法上,如果要修改只能改代码,明显很麻烦,实际项目中权限都是根据配置修改的,

所以就要用到自定义策略授权了。

第一步:

增加一个CustomAuthorizatinRequirement.cs,要求实现接口:IAuthorizationRequirement

  /// <summary>
/// 策略授权参数
/// </summary>
public class CustomAuthorizationRequirement: IAuthorizationRequirement
{
/// <summary>
///
/// </summary>
public CustomAuthorizationRequirement(string policyname)
{
this.Name = policyname;
} public string Name { get; set; }
}

增加CustomAuthorizationHandler.cs------专门做检验逻辑的;要求继承自AuthorizationHandler<>泛型抽象类;

   /// <summary>
/// 自定义授权策略
/// </summary>
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<IAuthorizationHandler, CustomAuthorizationHandler>(); 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增加测试方法

       /// <summary>
/// 进入授权策略
/// </summary>
/// <returns></returns>
[Authorize(policy: "customPolicy")]
public IActionResult roleData4()
{
return Content("自定义授权策略");
}

访问roleData4,看是否进到自定义授权策略逻辑

可以看到自定义授权策略生效了,授权策略就可以在这里做了,下面加上授权逻辑。

我这里的权限用路径和角色关联授权,加上授权逻辑后的校验代码。

/// <summary>
/// 自定义授权策略
/// </summary>
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; //登录成功时根据角色查出来这个用户的权限存到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

.NET Core中的鉴权授权正确方式(.NET5)的更多相关文章

  1. WebSocket 的鉴权授权方案

    引子 WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力.然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的: This protocol doesn’t pres ...

  2. websocket之四:WebSocket 的鉴权授权方案

    引子 WebSocket 是个好东西,为我们提供了便捷且实时的通讯能力.然而,对于 WebSocket 客户端的鉴权,协议的 RFC 是这么说的: This protocol doesn’t pres ...

  3. .Net 鉴权授权

    在这里总结一下工作中遇到的鉴权和授权的方法 ① 固定token的方案 通过在nginx或者代码中写死token,或者通过在限制外网访问的方式已来达到安全授权的方式 ② session方案 分布式会话方 ...

  4. asp.net Core 中AuthorizationHandler 实现自定义授权

    前言 ASP.NET Core 中 继承的是AuthorizationHandler ,而ASP.NET Framework 中继承的是AuthorizeAttribute. 它们都是用过重写里面的方 ...

  5. 第十五节:Asp.Net Core中的各种过滤器(授权、资源、操作、结果、异常)

    一. 简介 1. 说明 提到过滤器,通常是指请求处理管道中特定阶段之前或之后的代码,可以处理:授权.响应缓存(对请求管道进行短路,以便返回缓存的响应). 防盗链.本地化国际化等,过滤器用于横向处理业务 ...

  6. ASP.NET Core 中读取 Request.Body 的正确姿势

    ASP.NET Core 中的 Request.Body 虽然是一个 Stream ,但它是一个与众不同的 Stream —— 不允许 Request.Body.Position=0 ,这就意味着只能 ...

  7. 在.net core中数据操作的两种方式(Db first && Code first)

    在开发过程中我们通常使用的是Db first这种模式,而在.net core 中推荐使用的却是 code first 反正我是很不习惯这种开发模式 于是就搜寻整个微软的官方文档,终于找到了有关.net ...

  8. Swift中编写单例的正确方式

    在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的.一个状态管理的例子大家都很熟悉,那就是单例.使用Swift时,有许多方法实现单例,这是个麻烦事,因为我们不知道哪个最合适.这里我们来回顾一下单例 ...

  9. 在.NET Core 中收集数据的几种方式

    APM是一种应用性能监控工具,可以帮助理解系统行为, 用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题, 通过汇聚业务系统各处理环节的实时数据,分析业务系统各事务处理的交易路径和处理 ...

随机推荐

  1. 教你 4 步搭建弹性可扩展的 WebAPI

    作者 | 萧起  阿里云云原生团队 本文整理自<Serverless 技术公开课>,关注"Serverless"公众号,回复"入门",即可获取 Se ...

  2. harmony OS 开发工具安装

    harmony OS 开发工具安装 安装流程 安装完成 初始配置 双击打开 Running DevEco Studio requires the npm configuration informati ...

  3. js Promise用法

    Promise 英文意思是 承诺的意思,是对将来的事情做了承诺, Promise 有三种状态, Pending 进行中或者等待中 Fulfilled 已成功 Rejected 已失败 Promise ...

  4. PTA习题6-8 统计一行文本的单词个数 (15分)

    参考<c和指针>里面运用strtok函数打印空白标记符(如\n,\t)的程序改写而成的代码 在之前我自己写了一个60行的链表版本的统计程序 相比之下这个strtok函数的程序要简洁明了的多 ...

  5. vue基础-动态样式&表单绑定&vue响应式原理

    动态样式 作用:使用声明式变量来控制class和style的值 语法: :class/:style 注意:尽可能不要把动态class和静态class一起使用,原因动态class起作用的时间会比较晚,需 ...

  6. Flink sql 之 join 与 StreamPhysicalJoinRule (源码解析)

    源码分析基于flink1.14 Join是flink中最常用的操作之一,但是如果滥用的话会有很多的性能问题,了解一下Flink源码的实现原理是非常有必要的 本文的join主要是指flink sql的R ...

  7. [no_code][Beta]项目展示博客

    $( "#cnblogs_post_body" ).catalog() 团队项目链接 Beta阶段核心开发点: github 前端 github 后端 github OCR文档-含 ...

  8. 状压dp学习笔记(紫例题集)

    P3451旅游景点 Tourist Attractions 这个代码其实不算是正规题解的(因为我蒟蒻)是在我们的hzoj上内存限制324MIB情况下过掉的,而且经过研究感觉不太能用滚动数组,所以那这个 ...

  9. 「笔记」$Min\_25$筛

    总之我也不知道这个奇怪的名字是怎么来的. \(Min\_25\)筛用来计算一类积性函数前缀和. 如果一个积性函数\(F(x)\)在质数单点是一个可以快速计算的关于此质数的多项式. 那么可以用\(Min ...

  10. RecyclerView使用详解

    使用RecyclerView要引用对应的jar包,但最新版的项目中,不用引用也可以使用. implementation 'com.android.support:recyclerview-v7:27. ...