环境:,net 6+oracle 11g
使用identity框架实现了,真实用户的登录
使用JWT令牌保证了数据安全
使用Signalr框架实现了后端消息的主动推送
所需要的包:
后端代码;
第一步:先使用Identity框架建立用户与角色模型
dentity框架主要作用是,方便对用户以及用户的权限进行管理,创建两个实体后可以使用Manager<T>对其尽心管理,其内还封装了很多常用方法,例如;通过ID寻找name等。
框架还会对用户的密码经行加密处理,加密后在传入数据库当中。
1.MyRole类,dentity框架框架会默认自动生成一些列,可以不用自建列
2.Myuser类
3.为上方两个类设置上下文类,此处直接进行了配置,也可单独写一个配置类进行配置。
此处指明了表名是由于在连接Oracle的情况下,efcore转换成的SQL代码会把表明写的很长,所以指明具体表名。
4.在programe中对dentity框架进行注册
此处为了方便,所以把密码的要求降低了很多,实际应用中这是不好的。
对密码还有其他的配置可以,详情可自行查阅
最后,建立完实体之后记得,在nuget控制台中使用:Add-Mgration +迁移名,Update-Database进行数据库库表的创建。
第二步:对JWT Token进行配置
使用JWT,他相较于传统的session,在跨域方面更方便。JWT是无状态的,其内蕴含所需要的全部数据,省了服务器的开销,可以将其放在前端页面上。JWT中可以设置过期时间的,而不需要服务器注销。所以JWT多应用于分布式集群及API前后端分离的项目当中。
1.新建模型类,对用于对key与过期时间的读取
2.在appsettings.json文件中配置需要读取的key与过期时间,此处是为了方便,你也可以在VS自带的虚拟环境变量中配置,也可以直接配置在本地的环境变量中,或者配置在secret安全文件中
3.在programe中对JWT进行注册
4.添加JWT中间件
app.UseAuthentication();//JWT的中间件
第三步:signalr实现聊天功能。
signalr能够将数据从后端主动推送到前端页面,
适合于需要通知个及时消息的应用例如:聊天,公告等。
适用于更新频率高的应用,例如:游戏,GPS等。
ASP.NET Core 中的SignalR
可以自动处理连接管理。
可以同时向所有连接的客户端发送消息。 例如聊天室。
可以向特定客户端或客户端组,特定用户或用户组发送消息。
可以对其进行缩放,以处理不断增加的流量。
可以使服务器于客户端互相调用其方法
1.新建类继承Hub,并注入UserManager<T>,方便之后操作
2.在programe中添加SignalR服务
builder.Services.AddSignalR();//注册SignalR框架
3.在JWT服务中添加额外的配置,为了从前端读取到tokn令牌
(opt=>{
opt.Events = new JwtBearerEvents
{//websocket不支持自定义报文头
//所以在前端我们把WJT通过URL中的QueryStrig传递
//然后在服务器端的OnMessageReceived中把QueryStrig中的JWT读出给context.Token
//jsignalr内置JET验证,没有这个signalr接收不到jwt的token与claims
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];//***1
var path = context.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/ChatHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
4.添加SIGNALR中间件,后方的地址表示继承HUB类的地址,SIGNALR连接建立后,前端会访问此类中的方法。
app.MapHub<ChatHub>("/ChatHub");
5.在PROGRANME中注册跨域服务,因为是前后端分离的项目,所以需要前端与后端两台服务器,需要跨域穿透才能进行服务器的互相访问。此处的URL表示前端服务器的地址。
string[] urls = new[] { "http://localhost:5173" };//跨域穿透,表示前端页面IP
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.WithOrigins(urls)
.AllowAnyMethod().AllowAnyHeader().AllowCredentials();
});
});
第四步:登录验证的功能实现
1.新建controller类继承ControllerBase抽象类。
把Manager与JWT的配置注入。
[Route("api/[controller]/[action]")]
[ApiController]
public class IdentityController : ControllerBase
{
private readonly UserManager<MyUser> userMG;
private readonly RoleManager<MyRole> roleMG;
private readonly IOptions<JWTOptions> options;
//通过builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"))
//读取注册服务后要用IOptions<>及逆行依赖注入
//private readonly JWTOptions options;
public IdentityController(UserManager<MyUser> userMG, RoleManager<MyRole> roleMG, IOptions<JWTOptions> options)
{
this.userMG = userMG;
this.roleMG = roleMG;
this.options = options;
}
2.实现LOgin方法,
在controller类中
新建一个loginmodel模型类,类内有名子及密码两个属性。
login方法具体思路,使用UuserManager属性的方法,通过前端传入的name,找到对应的用户。
之后检测密码正确性。NEW一个List<Claim>对象,方便用于存储Claim,Hub 类具有一个 Context 属性,该属性可以获得与链接相关的用户的 ClaimsPrincipal ,所以我们把用户的某些信息加入List<Claim>后,再把这个对象加入JWT实现传值。
此外,构建JWT还需要,JWT的key,与过期时间。
最后返回JWT字符串。
[HttpPost]
//[SkipJwtVsersionFilter]
public async Task<ActionResult<string>> Login(LoginModel lml)
{
var user1=await userMG.FindByNameAsync(lml.Name);//取USER
Console.WriteLine(lml.Name,lml.PassWord);
var success = await userMG.CheckPasswordAsync(user1, lml.PassWord.ToString());//验证密码
if (!success) {//失败则返回
return BadRequest("failed please rewirte!!");
}
//user1.JwtVersion += 1;
//await userMG.UpdateAsync(user1);
//给JWT的PayLoad添加数据
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier,user1.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name,user1.UserName));
//为了在令牌中加入user的ROLE
var role = await userMG.GetRolesAsync(user1);
foreach (var r in role)
{
claims.Add(new Claim(ClaimTypes.Role,r));
}
string key = options.Value.Key;
//签名密钥,要求至少32位,从已注入的配置中取
double time = options.Value.ExporeSeconds;
//Console.WriteLine(time);
DateTime ex = DateTime.Now.AddSeconds(time);
//过期时间
// 配置令牌的代码,照抄以前的代码
byte[] secBytes = Encoding.UTF8.GetBytes(key);
SymmetricSecurityKey secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
//head
var tokenDescriptor = new JwtSecurityToken(claims: claims,
expires: ex, signingCredentials: credentials);
//payload
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
//signature
//最后生成的JWT
return jwt;
}
第五步:群聊与私聊功能实现
1.在上方创建的继承HUB抽象类的类中写入群聊方法
具体思路:从前端去的name以后直接使用群聊方法对所有人发送消息,
注意Send A sync后第一个变量要和前端监听返回值的方法中的变量名相同
public Task PublicAllMessage(string msg)
{
var userName = this.Context.User.FindFirstValue(ClaimTypes.Name);
var time=DateTime.Now;
string returnmsg = $"==用户=={userName}==说:{msg}------{time}";
this.Clients.All.SendAsync("userMessage", returnmsg);//userMessag时前端接收变量的名字
return Task.FromResult("ok");
}
2.在此类中写入私聊方法
具体思路:前端给予私聊接者的Name,通过其取得具体user和Id,在Hub类context属性中取得发送者与ID之后,构建完数据字符串之后。首先使用.User()方法对发送者发送消息。只有由于发送者并不知道是否成功发送,所以对于发送者也要同步发送消息。但是当给自己发送消息时,会出现两条消息,所以,对于消息同步及你想一个判断,如果给自己发消息就不同步。
public async Task<string> SendPrivateMessageAsync(string toUserName, string Message, string senderName)
{
var user1 = await hubUser.FindByNameAsync(toUserName);
string toUserId = user1.Id.ToString();
var senderUserId = this.Context.UserIdentifier;
string senderUserName = this.Context.User.FindFirstValue(ClaimTypes.Name);
var a = $"{senderUserName}====给用户={toUserName}发送:------{Message}";
await this.Clients.User(toUserId).SendAsync("privateMsg", a);//给对方客户端推送
if (senderUserId!=toUserId) {
await this.Clients.User(senderUserId).SendAsync("privateMsg", a);}
//给对自己客户也要推送,表示知道消息发出,如果是自己给自己发则不用,否则会同时显示两条消息
return "ok";
}
第六步:前端代码
使用代码前,记得安装axios
具体实现思路就是像后端Login发送axios请求,如果登录成功就建立SignalR连接。
通过触发方法中的Connection.Invoke()方法调用后端的私聊与群聊方法。
登录成功后,使用Connection.ON()方法监听后端传回的值,放到之后页面上显示
<script >
import {defineProps,reactive,onMounted} from 'vue';
import * as signalR from '@microsoft/signalr';
import axios from 'axios';
let connection;
export default{
//name:"Login",
setup()
{
const state=reactive({userMessage:"",messages:[],pmessages:[],uname:"",upassword:"",privateMsg:"",touser:""});//页面上两个睡醒
//const privatest=reactive({userMessage:"",messages:[],uname:"",upassword:"",touser:""})
const login1=async function()
{
//请求数据的名字要和后端的名字一样才可以
const payload={Name:state.uname,PassWord:state.upassword};//请求数据的名字要和后端的名字一样才可以
axios.post("https://localhost:7176/api/Identity/Login",payload)//向后端(登陆验证)发送请求,亲求payload中的数据,包括TOKEN
.then(async suc=>{//login success
const token=suc.data;//api/Identity/Login方法的返回值
const options={skipNegotiation:true,transport:signalR.HttpTransportType.WebSockets}//身份验证通过就建立signalr连接
options.accessTokenFactory=()=>token;
connection=new signalR.HubConnectionBuilder()//通过signalR与服务器建立连接
.withUrl('https://localhost:7176/ChatHub',options)//把token等连接字串,通过URL的QueryString传给后端的ChatHub
.withAutomaticReconnect().build();
await connection.start();//开始连接
alert("Login success");
connection.on("privateMsg",msg => {
state.messages.push(msg);
//alert(msg);
});//监听私聊,意思时signalr连接在就监听
connection.on("userMessage",msg=>{
state.messages.push(msg);
});//监听后端传回的值,放到state的messages中
})
.catch(err=>{//login defead
alert("please rewirte!!!!!!!!!!!!");
});
};
const privateMsgFunction=async function(e){
if(e.keyCode!=13) return;
await connection.invoke("SendPrivateMessageAsync",state.touser,state.privateMsg,state.uname);
state.privateMsg="";
};
const textMsgOnkeypress=async function(a)
{
if(a.keyCode!=13) return;
await connection.invoke("PublicAllMessage",state.userMessage);//调用后端PublicAllMessage方法,并传值
state.userMessage="";
};
// onMounted(async function()
// {
// // connection=new signalR.HubConnectionBuilder()//通过signalR与服务器建立连接
// // .withUrl('https://localhost:7176/ChatHub'
// // ,{skipNegotiation:true
// // ,transport:signalR.HttpTransportType.WebSockets
// // ,accessTokenFactory:state.token})//从后端取的值
// // .withAutomaticReconnect().build();
// // await connection.start();
// // connection.on("userMessage",msg=>{
// // state.messages.push(msg);
// // })//监听后端传回的值,放到state的messages中
// });
return {state,textMsgOnkeypress,login1,privateMsgFunction};
}}
</script>
<template>
<h3>一个登录一个用户,不同用户间实时交流</h3>
<p></p>
<div>
账户:<input type="text" v-model="state.uname">
密码:<input type="password" v-model="state.upassword">
<button v-on:click="login1">登录</button>
</div>
<p>私聊功能</p>
<div>
私聊用户名称:<input type="text" v-model="state.touser"/>
私聊内容:<input type="text" v-model="state.privateMsg" v-on:keypress="privateMsgFunction"/>
<!-- <button v-on:click="privateMsgFunction">发送</button> -->
<p>私聊功能</p>
</div>
<h4>群聊请输入文字:</h4>
<input type="text" v-model="state.userMessage"
v-on:keypress="textMsgOnkeypress"/>
<div>
<ul>
<li v-for="(msg,p) in state.messages" :key="p">{{msg}}</li>
</ul>
</div>
</template>
具体页面效果,我前端不行,写起来太麻烦了,好不好看无所谓,能实现具体功能就行了
总结:
加深了对
identity框架实,JWT令牌,Signalr的印象,
而且找到了自己写的代码的BUG,虽然是因为自己失误产生的BUG,但是自己花了两天一点点排除问题,解决了问题,还是收获了很多。