用于web项目的简单无密码身份验证

2022-02-21 11:36:15

我讨厌密码。大多数现代浏览器和密码管理器基本上解决了密码管理问题。我讨厌的是作为一个开发人员来处理他们。散列、存储、身份验证等。

我最近用我的套接字做了一个小项目。io同步了vuex状态,需要一个用户可以轻松登录的系统。我将在此声明,这正是我为我的个人项目所做的,安全性并不重要。如果登录被窃取,那就是我和朋友玩的一个愚蠢的游戏。所描述的技术不应在生产中使用,除非经过一些改进。如果你对如何以更安全的方式实现这一点有想法,一定要联系我!

我使用express作为服务器,所以我将把它作为基准。我也在使用TypeScript,因为你为什么不使用它呢?在设置服务器时,我有一个控制器类型的文件,可以传入。这是我的服务器文件:

//服务器代码从"导入express;快递";;从"导入bodyParser;体分析器";;从"导入路径;路径";;从"导入http;http";;从';导入AuthController/控制器/auth';;//配置应用和文件夹位置const app=express();//每次启动数据库const db=GetDB()时重置数据库;db。connect();//服务静态内容const server=http。createServer(应用程序);AuthController(应用程序,数据库);应用程序。使用(express.static(client_文件夹));应用程序。获取(";/api/*";,(请求、回复)=>;{res.status(404).send(";找不到";);})应用程序。post(";/api/*";,(请求、回复)=>;{res.status(404).send(";找不到";);})应用程序。获取(';*';,(请求、回复)=>;{res.sendFile(path.resolve(client_folder,';index.html';);});服务器听(app.get(";port";),()=>;{console.log(";应用程序正在运行http://localhost:%d在%s模式下";,应用程序。获取(";端口";),应用程序。获取(";env";);安慰日志(";按CTRL-C停止\n";););

它被修剪了一段,但你可以知道我要去哪里。这是我手工修改代码的过程,所以不要指望复制并粘贴这些代码,然后去参加比赛。

你可能会注意到我有一个叫做auth控制器的东西。auth控制器是魔法发生的地方。

从"导入{Express};快递";;导出默认函数RegisterEndPoints(app:Express,db:DataBase){app.post(apidentroot+apidentpoints.LOGIN_TEMP,async(req,res)=>;{//…});//神奇链接登录应用。get(ApiEndpointRoot+ApiEndpoints.LOGIN_MAGIC,async(req,res)=>;{ // ... }); // 尝试登录用户应用程序。post(APINDPROOT+APINDPROOT.LOGIN,异步(req,res)=>;{ // ... }); // 检查我们是否';重新登录应用程序。使用(异步(req、res、next)=>;{ // ... }); 应用程序。获取(ApiEndpointRoot+ApiEndpoints.LOGOUT,(req,res)=>;{//清除登录令牌res.clearCookie(';令牌';);res.重定向(";/";);)

为了简洁起见,我删除了一些代码。在这个项目中,客户机和服务器位于同一个repo中,并且构建在一起。他们有一个名为common的文件夹,其中包括为游戏提供动力的状态机、API端点定义和公共类型。从开发的角度来看,确保服务器和客户端不会失去同步非常方便,因为typescript捕获了很多东西。这并不能让它变得简单(浏览器缓存对于奇怪的bug来说可能很棘手),但随着项目规模的扩大,它解决了很多问题。

有几个助手函数,主要是关于读取和写入JWT令牌的。

导出函数DecodeJwtToken(令牌:字符串):JwtUser |null{const results=(JwtDecode(令牌)如有);if(results==null)返回null;const user:JwtUser={name:results.name,_id:results._id,temporary:results.temporary,};返回用户;}函数GiveToken(token_user:JwtUser,res:any,message:string,temporary?:boolean){if(temporary==undefined | | temporary==null)temporary=false;const expireInHours=temporary?24:10000;//大约一年const token=JwtSign(token_user,jwtu SECRET,{expiresIn:expireInHours+';h';});res.cookie(';token';,token,{maxAge:1000*60*60*expireInHours,secure:true});如果(消息!=';';){res.json({token,message});}函数生成器magiccode(){const magic_key_length=25;const characters=';abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzo123456789';;const charactersLength=characters.length;let result=Array(magic#key_length)。fill(';';';)。地图((x)=>;角色。charAt(Math.floor(Math.random()*charactersLength)))。加入(';';);返回结果;}

有三个功能:一个用于解码令牌,一个用于给出令牌,一个用于生成魔法代码。令牌存储在名为token的浏览器cookie中。将来,在令牌中编码某种特定于浏览器的指纹会很好。或者其他一些机制来防止cookie从浏览器中被盗并被使用。也许在未来,可以使用某种刷新令牌机制。现在,一个会议将持续很长时间。在未来,可能会有一个不经常传输的刷新舞蹈(可能在本地存储或其他地方)。

导出默认函数RegisterEndPoints(app:Express,db:DataBase){/..app.post(apidemproot+apidemps.LOGIN_TEMP,async(req,res)=>;{try{const new_user_data:user={email:';';,name:RandomName(),temporary:true,}让新用户=wait db。用户添加(新用户数据);if(new#u user==null){res.status(500).send(";无法创建临时用户";);返回;}const token_user:JwtUser={_id:new_user._id,name:new_user.name,temporary:true,};GiveToken(token#u user,res,";创建了新的临时帐户";,true);返回;}catch(e){console.error(";LoginUserTemp error:";+e);res.status(500)。send(";Not implemented(";);})//。。。

基本上,我们在数据库中生成一个新用户,将其标记为临时用户。任何被标记为临时且使用时间超过36小时的帐户都将从数据库中清除。我们给他们一个只持续24小时的代币,而且无法升级到永久账户。

导出默认函数RegisterEndPoints(app:Express,db:DataBase){//……//尝试登录用户app.post(apidentroot+apidentpoints.login,async(req,res)=>;{if(req.body[';email';])=未定义){res.status(300)。发送(";电子邮件丢失";);返回;}const email=req。正文[';电子邮件';];如果(请求正文[';电子邮件';])='') {res.status(300)。发送(";电子邮件空白";);返回;}const valid_email=validateEmail(电子邮件);如果(!valid_email){res.status(300)。发送(";email无效";);返回;}let user=wait AttemptLoginOrRegister(数据库,电子邮件);if(user==null){res.status(300).send(";无法创建新帐户";);返回;}如果(用户==';电子邮件';){//告诉用户检查他们的电子邮件资源。发送(";检查电子邮件";);返回;}const-token_-user:JwtUser={u-id:user._-id,name:user.name,temporary:user.temporary | | false,};GiveToken(token#u user,res,";created user";);});

在这里,我们期待一个post请求,其中包含一封电子邮件。我们验证电子邮件(您需要提供此功能),然后调用AttemptLogin。如果用户已经存在,我们会返回电子邮件,这是一个愚蠢的设计,告诉我们帐户已经存在。否则,将创建一个新用户。

//尝试登录给定的电子邮件,如果它们已经存在,那么异步函数AttemptLoginOrRegister(db:DataBase,email:string):Promise<;DbUser | null |和#39;电子邮件'>;{试试{if(email==';';)返回null;//第一步:检查用户是否已经存在,如果已经存在,返回email const user=wait db。userFind(电子邮件,空);//用户存在,设置他们的魔法代码并返回if(user!=null){//TODO:生成一个魔法东西,并将其设置到他们的用户const magic_code=GenerateMagicCode();user.magicCode=magic_code;sendMagicCodeEmail(user,magic_code);console.log(";http://localhost:3000" + ApiEndpointRoot+ApiEndpoints。登录_MAGIC+";?代码=";+魔法代码+"&;id=";+用户_身份证);等待db。用户更新(用户);返回';电子邮件';}const name_parts=电子邮件。分裂(';@';);const name=name_parts[0];//第二步:用户不';t exist所以我们需要创建它们const new_user_data:user={email,name,}let new_user=wait db。用户添加(新用户数据);if(new_user==null)返回null;返回新用户;}catch(e){console.error(";AttemptLoginOrRegister error:";+e);返回null;}}

简而言之,如果他们试图登录,我们会在数据库中创建一个魔法代码,并将其发送到他们的电子邮件中。否则,如果是一封独特的电子邮件,请创建一个新帐户并登录。默认情况下,他们的用户名是电子邮件的第一部分。然而,用户名不是唯一的,电子邮件是唯一的。

导出默认函数RegisterEndPoints(app:Express,db:DataBase){//…//magic link login app.get(apidentroot+apidentpoints.login_magic,async(req,res)=>;{if(req.query[';code';])=未定义){res.status(300).发送(";代码缺失";);返回;}如果(请求查询[';id';])=未定义){res.status(300).send(";id缺失";);返回;}const id=parseInt(请求查询[';id';])。toString());const user=wait db。userFind(null,id);if(user==null){res.status(300).send(";找不到用户";);返回;}const magic=req。查询[';代码';];const curr_magic=用户。magicCode;//如果(user.magicCode!=';';)出现以下情况,请删除魔法代码{user.magicCode=';';db.userUpdate(user);}//检查他们是否没有';如果(curr|u magic==null | | curr|u magic==undefined | | user.temporary | | curr| u magic==';&| magic!=curr|u magic){res status(300)。发送(";magic code不匹配";)//TODO:删除魔法代码?返回;}const-token_-user:JwtUser={u-id:user._-id,name:user.name,temporary:user.temporary | | false,};res.status(200)GiveToken(token#u user,res,";";)//res.send(";<;script>;window.location.replace(';/';)</脚本>") res.重定向(";/";)返回});

我们通过sendgrid向用户发送一封电子邮件,其中包含指向该端点的链接(不受赞助,只是易于使用)。

导出默认函数RegisterEndPoints(app:Express,db:DataBase){/…//检查我们是否重新登录app.use(async(req,res,next)=>;{const path=req.path;if(path=';/favicon.ico';| path.startsWith(&;/39 js';)|路径开始(';/img/';)|路径开始(';/css/';)|路径=='/登录';|124;路径indexOf(';';)!=-1) {return next();}请尝试{//console.error(";检查auth以获得";+path);const token=(请求cookies)?请求cookies[';token';]:请求。标题。批准分割(";持票人";)[ 1]; 如果(!token)抛出新错误(";无授权头";);等待JwtVerify(令牌,JWT_机密);当地人。令牌=令牌;const results=(JwtDecode(令牌)如有);//TODO:检查用户是否确实存在?当地人。用户=结果;返回next();}catch(e){//console.error(";Auth check";e)}/TODO:如果我们';如果(path.startsWith(';/api/';)| |路径=='/注销(#39;){return next();}//重定向到登录控制台。错误(";从";+请求路径+";重定向到/登录";);return res.redirect(';/login';);});

差不多就这些了。用户只需通过电子邮件创建一个帐户,也可以创建一个临时帐户。他们的会议持续了很长时间。如果过期或他们试图从其他浏览器登录,他们会在电子邮件中获得一个代码。对于一个流量不大的简单网站来说,这是一个很好的解决方案。