专业编程基础技术教程

网站首页 > 基础教程 正文

Blazor中Session认证登录极简示例,绝对可用!

ccvgpt 2025-01-13 11:18:17 基础教程 9 ℃

前言

在 Blazor 中,认证登录向来是个棘手的难题。在网络之上,能够找到的完整示例基本上没有。历经周折方才拼凑出一个可用的方案。本示例乃是一个 Session 认证登录的范例,肯定可用。倘若您依此例仍无法使程序得以运行,可私信于我索求此示例的源工程代码。

为了避免版本差异导致的不可用,请仔细阅读注意事项:

Blazor中Session认证登录极简示例,绝对可用!

1、此项目通过BootStrapBlazor项目模板直接创建

2、此项目使用的是纯Server模式,.net 8.0框架

3、使用了SQL Server 2016版本,数据库连接采用FreeSql,开发使用VS2022,系统为win10 22H2版本


项目文件说明

使用bootstrapblazor模版创建基于Server的工程后,删除了其中可能影响你编写代码的示例,增加了Session认证的部分,最终保留的文件如下

API:实现Session登录认证和退出的API控制器

Login:实现登录的页面和模板文件

OA.cs:实现密码加密的功能,无关的代码已清理掉了

UserList.cs:数据实体类,用户的信息存放于此,相应的数据库中也得有相同的表

上面这些都是新增的,其余的为删除后保留的,你也可以不删除,不影响运行。

数据实体类和OA类

数据实体类直接复制代码便可用,登录只使用到了用户_账户表,其他的可以删除,在数据库中,表名和字段都是中文名称,如果你的不是,请修改这里的实体类就行。

public class UserList
{

}
[Table("用户_账户")]
public class UserAccout
{
    [Key]
    public int UID { get; set; }
    [Required(ErrorMessage = "用户名不能为空")]
    public string? 用户名 { get; set; }
    [Required(ErrorMessage = "密码不能为空")]
    public string? 密码 { get; set; }
    public string? 姓名 { get; set; }
    public DateTime? 登录日期 { get; set; }
    public int? 角色ID { get; set; }
}
   

[Table("用户_角色")]
public class UserRole
{
    [Key]
    public int UID { get; set; }
    public string? 角色名称 { get; set; }
    public int? 角色ID { get; set; }
}

[Table("用户_权限")]
public class UserQx
{
    [Key]
    public int id { get; set; }
    public string? 用户名 { get; set; }
    public string? 权限id { get; set; }
    public string? 状态 { get; set; }
}

OA类,对用户输入的密码进行加密,并返回加密后的字符串

        /// <summary>
        /// SHA256加密,返回加密后的字符串
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public static string SHA256EncryptString(string data)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(data);
            byte[] hash = SHA256.Create().ComputeHash(bytes);

            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < hash.Length; i++)
            {
                builder.Append(hash[i].ToString("x2"));
            }
            return builder.ToString();
        }

登录验证API控制器

新建API文件夹,新建控制器文件AccountController,可直接复制使用,FreeSql使用构造注入

[Microsoft.AspNetCore.Mvc.Route("api/[controller]/[action]")]
[ApiController]

public class AccountController : ControllerBase
{
	private readonly IFreeSql fsql;
    public AccountController(IFreeSql _fsql) 
    {
        fsql = _fsql;
    }
	public object Login([FromBody]UserAccout user) 
    {

        var t = fsql.Select<UserAccout>().Where(p=>p.用户名==user.用户名 && p.密码==user.密码).ToOne();
        if (t != null)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.Name, user.用户名));



            HttpContext.SignInAsync(new ClaimsPrincipal(identity), new AuthenticationProperties()
            {
                IsPersistent = true,
                ExpiresUtc = DateTimeOffset.Now.AddHours(8)
            });
            return new { code = 2000, Message = "登录成功" };
        }
        else
        {
            return new { code = 5000, Message = "登录失败" };
        }
        
    }

    [HttpGet]
    [Authorize]
    public async Task<IActionResult> Logout() 
    {
        await HttpContext.SignOutAsync();
        return Redirect("/login");
    }
}

登录页面

新建login文件夹,新建登录页面和模板,用于实现登录功能,登录页面改造于bootstrapblazor的示例,简化掉了一些不必要的元素。

Login.razor界面,@attribute [AllowAnonymous]表示所有人都可以访问,不需要权限

@page "/Login"
@layout LoginLayout
@inject NavigationManager NavigationManager
@attribute [AllowAnonymous]
	<div class="login-item">
		<div class="text-center" style="font-size:18px; padding-bottom:30px;">
			
		</div>
		<ValidateForm Model="@user" OnValidSubmit="SubmitHandler">
			<BootstrapInput @bind-Value="@user.用户名" ShowLabel="false" PlaceHolder="请输入账号" />
			<BootstrapPassword @bind-Value="@pwd" ShowLabel="false" PlaceHolder="请输入密码" />
			<hr />
			<Button Text="登录" Color="Color.Success" ButtonType="ButtonType.Submit"></Button>
		<br /><br />
		</ValidateForm>
	</div>

Login.razor.cs代码文件,如果没有登录的话转到登录页,如果已经登录转到主页。


using Test.Data;
using Test.Models;
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualBasic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;



namespace Test.Components.Login
{
    public partial class Login
    {
    
        private UserAccout user { get; set; }=new UserAccout();
        private string pwd { get; set; } = "";

        //弹窗提示
        [Inject]
        [NotNull]
        private SwalService? SA { get; set; }
        [Inject] AjaxService? Ajax { get; set; }
        [Inject] NavigationManager NM { get; set; }
        [Inject]AuthenticationStateProvider AUTH { get; set; }
        [Inject]IFreeSql fsql{ get; set; }
		private async Task SubmitHandler(EditContext arg)
        {
            
            var t=fsql.Select<UserAccout>().ToList();

			if (string.IsNullOrEmpty(user.用户名) || string.IsNullOrEmpty(pwd))
            {
                await SA.Show(new SwalOption()
                {
                    Category = SwalCategory.Information,
                    Content = #34;用户名或密码不能为空!",
                });
            }
            else 
            {
                user.密码 = OA.SHA256EncryptString(pwd);
                var doc=await Ajax.InvokeAsync(new AjaxOption() 
                {
                    Url="/api/Account/Login",
                    Data=user,
                });
                if (doc?.RootElement.GetProperty("code").GetInt32() == 2000)
                {
                    await Ajax.Goto("/");
                    await SA.Show(new SwalOption()
                    {
                        Category = SwalCategory.Information,
                        Content = #34;登录成功!",
                    });
                }
                else 
                {
                    await SA.Show(new SwalOption()
                    {
                        Category = SwalCategory.Information,
                        Content = #34;登录出错!",
                    });
                }
            }
            

		}

        protected override async void OnInitialized()
        {
            base.OnInitialized();
            var user=(await AUTH.GetAuthenticationStateAsync()).User;
            if (user?.Identity?.IsAuthenticated==true) 
            {
               NM.NavigateTo("/");
            }
        }
    }
}

登录页使用的LoginLayout模板文件

@inherits LayoutComponentBase

<html >
<body >
	<BootstrapBlazorRoot>
	<div class="container">
		<div class="row" style="padding-top:260px;">
			<div class="col-12">
				<Card class="shadow-lg border-0 my-5">
					<HeaderTemplate>
						<div style="text-align:center;width:100%;font-size:18px;">
							欢迎使用内部系统
						</div>
					</HeaderTemplate>
					<BodyTemplate>
						@Body
					</BodyTemplate>
				</Card>
			</div>
		</div>
	</div>
	</BootstrapBlazorRoot>
	</body>
</html>

模板的样式文件

.layout-drawer-body {
    padding: 1rem;
    background-color:red
}

    .layout-drawer-body ::deep .groupbox {
        margin-top: 1rem;
    }

    .layout-drawer-body ::deep .btn-info {
        margin-bottom: 1rem;
    }

.layout-item {
    --bb-layout-sidebar-bg: #f8f9fa;
    --bb-layout-footer-bg: #e9ecef;
    cursor: pointer;
    border: 2px solid #e9ecef;
    padding: 4px;
    border-radius: 4px;
    height: 80px;
    width: 120px;
    transition: border .3s linear;
}

    .layout-item:hover,
    .layout-item.active {
        border: 2px solid #28a745;
    }

    .layout-item .layout-left {
        width: 30%;
        border-right: 1px solid var(--bs-border-color);
    }

        .layout-item .layout-left .layout-left-header {
            height: 16px;
            background-color: var(--bb-header-bg);
        }

        .layout-item .layout-left .layout-left-body,
        .layout-item .layout-body .layout-left {
            background-color: var(--bb-layout-sidebar-bg);
        }

    .layout-item .layout-right .layout-right-header,
    .layout-item .layout-top {
        background-color: var(--bb-header-bg);
        height: 16px;
    }

    .layout-item .layout-right .layout-right-footer,
    .layout-item .layout-right-footer {
        background-color: var(--bb-layout-footer-bg);
        height: 12px;
    }

    .layout-item .layout-top,
    .layout-item .layout-body,
    .layout-item .layout-right-footer {
        width: 100%;
    }

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.back-main {
    background-color: aqua;
    background-image: url(../images/bg.jpg);
    background-position: center;
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
    overflow: hidden;
    margin: 0;
    padding: 0;
}

.container {
    --login-max-width: 540px;
    --login-padding-x: 1.5rem;
    --login-padding-y: .75rem;
    --bs-border-radius: 10px;
    max-width: var(--login-max-width);
    margin: 0 auto;
}

::deep h4 {
    margin-bottom: 1.5rem;
}

::deep .form-control {
    --bb-form-control-padding: var(--login-padding-y) var(--login-padding-x);
    font-size: .875rem;
    margin-bottom: 1rem;
}

::deep .btn {
    --bs-btn-padding-x: var(--login-padding-x);
    --bs-btn-padding-y: var(--login-padding-y);
    width: 100%;
    margin-bottom: 1rem;
}

    ::deep .btn:first-of-type {
        margin-bottom: 0;
    }

::deep .form-check {
    padding: var(--login-padding-y) var(--login-padding-x);
    margin-bottom: 1rem;
}

html {
    height: 100%;
    background-image: url(../images/bg2.jpeg);
    background-position: center;
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
    overflow: hidden;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;

}
body {
    background-image: url(../images/bg2.jpeg);
    background-position: center;
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-size: cover;
}



跳转组件,应该是没用,当时留下来做测试用的

<h3>RedirectToLogin</h3>
@inject NavigationManager Nav
@code {
    protected override void OnInitialized()
    {
        base.OnInitialized();
        Nav.NavigateTo("/Login", true);
    }
}

其他原有文件的修改

_imports.razor文件,在最下面增加一句@attribute [Authorize],表示需要通过认证才能访问

routes.razor文件,在.net8.0中需要写成下面这样子,和以前不一样了,默认使用的模板是MainLayout

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
</Router>

program.cs文件,app.MapDefaultControllerRoute();这一句一定要有,不然在登录时不会执行API控制器中的内容,FreeSql服务也在这里注入。

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.SignalR;
using System.Text;
using Test.Components;
using Test.Data;
using FreeSql;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

builder.Services.AddRazorComponents().AddInteractiveServerComponents();

IFreeSql _fsql = new FreeSql.FreeSqlBuilder()
        .UseConnectionString(FreeSql.DataType.SqlServer, "Data Source=192.168.3.111;User Id=sa;Password=数据库密码;Initial Catalog=数据库名称;Encrypt=True;TrustServerCertificate=True;Pooling=true;Min Pool Size=1")
        .Build();

builder.Services.AddSingleton<IFreeSql>(_fsql);

builder.Services.AddBootstrapBlazor();



//添加压缩中间件,不然部署到IIS上时会报错
builder.Services.AddResponseCompression();
builder.Services.AddControllers();
builder.Services.AddAuthenticationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();


//添加Cookies认证中间件
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.LoginPath = "/login";
});

// 增加 SignalR 服务数据传输大小限制配置
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseResponseCompression();
}

//出错直接使用自带的error模版,而不是路由中的
app.UseStatusCodePagesWithRedirects("/Error");
//添加Cookie认证中间件
app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
//结束

app.UseStaticFiles();

app.UseAntiforgery();

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();




用户登录以后,如何在其他页面中获取登录的用户账号信息了,以index.razor为例,

@page "/"
@attribute [TabItemOption(Text = "Index", Closable = false)]

<PageTitle>Index</PageTitle>

<h1>各位好!</h1>

欢迎使用Blazor程式,当前登录用户为:@UserName

@code
{
	private string? UserName { get; set; }
	private string? Name { get; set; }
	[Inject] AuthenticationStateProvider Auth { get; set; }
	protected override async void OnInitialized()
	{
		base.OnInitialized();
		var user = (await Auth.GetAuthenticationStateAsync()).User;
		UserName = user.Identity.Name;

	}

}

获取到用户账号信息后,就可以通过用户账号去查询用户是什么角色,具有哪些权限了,到此就已经很清晰的说完了。

在这里无法上传附件信息,有需要的请私信交流。

最近发表
标签列表