一个简单的web sso实现方案

其实仅实现web单点登录并没有太多的开发工作量,当然如果要实现一整套符合oauth2.0标准的认证系统,并且加上对接外部认证源,应用注册管理,系统日志管理,资源管理,安全管理等就不是那么简单的一项任务了。大家可以搜一下开源项目参考学习。以下我们前端用javascript,后端用nodejs+express来手写一个web sso。
先介绍下技术方案:
在这里插入图片描述
蓝色部分是app侧express的拦截器里要实现的功能;红色部分是sso侧要提供的服务功能:包括登录页面、ticket生成、ticket查询等。ticket以query参数的方式附加在url里传递。
将sso的session存放在redis中,这样sso服务器还可以扩展到多节点;将客户端登录sso的sid(会话ID和IP经过加密算法生成),存放到客户端cookie,利于后续服务器取得比对防伪;还将ticket也存放在redis,生成后一次查询后立即删除。
该sso方案包含了密码加密传输和登录失败策略。
以下是代码实现。

//app侧后端
function ssocheck(redurl,queryticketurl,ssourl,logger,postProcess,req,res,next){
	if (!req.session.user) {
		//logger.info("未登录该应用");
		let ticketid=req.query.ticket;
		if (ticketid!=undefined){
			//logger.info("有票据");
			axios.get(queryticketurl+ticketid).then(resax => { 
				//logger.info("查询反馈数据: "+JSON.stringify(resax.data));
				if (resax.data.msg!='not found') {
					postProcess(resax.data.ssouser,req,res,next);//取得sso用户后的app处理,比如查询app用户等操作
					}
				else { 
					//logger.error("无效ticket");
					let urlParsed = new URL(redurl);
					urlParsed.searchParams.delete("ticket");
					let newredurl=urlParsed.toString()
					res.redirect(ssourl+newredurl); 
					}
				})
				.catch(errorax => { 
					//logger.error("查询ticket出错");
					res.json({msg:"查询ticket出错"}); });
			}
		else {
			//logger.info("无票据 重定向到sso")
			res.redirect(ssourl+redurl); 
			}
	 	}
	else  { next(); }
}

app.use((req,res,next)=>{

	if (req.url=="/") { ... }
	else if (...) { next(); }
	else { let redurl=app_prefix+ req.originalUrl; ssocheck(redurl,queryticketurl,ssourl,logger,postProcess,req,res,next); }
});
//sso前端 为避免口令明文在网络上传递,采用加密方式,当然首先是必须使用https的
function login(){
	if ((document.getElementById('uid').value.trim()=="")||(document.getElementById('upw').value.trim()=="")) { alert("请输入用户名和密码!"); }
	else {
		$.get("/sso/reqlogin",(res)=>{
			let fupw=document.getElementById('upw').value;
			let ekey=CryptoJS.enc.Hex.parse(res.key);
			let eiv=CryptoJS.enc.Hex.parse(res.iv);
			let enpw=CryptoJS.AES.encrypt(fupw, ekey, {
					iv: eiv,
					mode: CryptoJS.mode.CBC,
					padding: CryptoJS.pad.Pkcs7,
					});
			let enpwstr=enpw.ciphertext.toString();
			$.post("/sso/login",{"reqid":res.reqid,"uid": nuid,"upw": enpwstr},(obj)=>{
				if (obj.msg=="pass") {
					var reg = new RegExp("(^|&)" + "redurl" + "=([^&]*)(&|$)","i");
					var r = location.search.substr(1).match(reg);
					if (r!=null) { document.location =r[2]+'?ticket='+obj.ticket;  } 
					else { document.location="/sso/apps.html"; }//sso本身也可以直接访问,放几个应用图标好直接点入
					}
				else { alert(obj.msg); }
				})
			})
		}
	}
//sso后端
function getsidfromcookie(cookiestr){ 
	let cookie = {}
	let cookies = cookiestr ? cookiestr.split(';') : []
	if (cookies.length > 0) {
		cookies.forEach(item => {
			if (item) {
				let cookieArray = item.split('=')
				if (cookieArray && cookieArray.length > 0) {
					let key = cookieArray[0].trim()
					let value = cookieArray[1] ? cookieArray[1].trim() : undefined
					cookie[key] = value
					}
				}
			})
		}
	return cookie["sid"]
	}

app.use((req,res,next)=>{

	if (req.url=="/") { res.redirect('/sso/apps.html'); }
	else if (...) { next(); }
	else {

		let getsid=getsidfromcookie(req.headers.cookie); //客户端cookie中的sid与redis中保存的sid比对,一致则通过,否则重新登录

		if (getsid==undefined) {
			if (req.query.redurl!=undefined) { res.redirect('/sso/login.html?redurl='+req.query.redurl);} else { res.redirect('/sso/login.html'); }
			}
		else  { 
			let clientip = req.header('x-forwarded-for')?req.header('x-forwarded-for'):req.ip;
			let csid=myencrypt(clientip+req.sessionID);

			if ((getsid!=csid)||(!req.session.user)) {
				if (req.query.redurl!=undefined) { res.redirect('/sso/login.html?redurl='+req.query.redurl);} else { res.redirect('/sso/login.html'); }
				}
			else { next(); }
			}
		}
});	

app.post('/login',urlencodedParser,(req,res)=>{

	let obj= req.body;
	let clientip=req.header('x-forwarded-for')?req.header('x-forwarded-for'):req.ip;
	logger.info(obj.uid+" 尝试登录");
	let now=new Date();
	let item=loginfaillist.find(itm=>(itm.ip==clientip));

	if ((item!=undefined)&&(now<item.permittime)) { res.json({msg:"该IP已触发登录异常,请于"+item.permittime.toLocaleTimeString()+"后再尝试"}); }
	else {
		client.get(obj.reqid+clientip).then(val=>{
			if (val==null) { logger.error("认证出错 "+obj.uid); req.session.destroy(); res.json({msg:"认证出错"}); addfail(now,clientip); }
			else {
				let lk=val.substr(0,32); let iv=val.substr(32);
				client.del(obj.reqid+clientip);
				let encryptedHexStr = CryptoJS.enc.Hex.parse(obj.upw);  let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);

				let depw=CryptoJS.AES.decrypt(srcs, CryptoJS.enc.Hex.parse(lk), {
					iv: CryptoJS.enc.Hex.parse(iv),
					mode: CryptoJS.mode.CBC,
					padding: CryptoJS.pad.Pkcs7,
					});
				let depwstr=CryptoJS.enc.Utf8.stringify(depw);

				callauthenticate(obj.uid, depwstr, (err)=> {//替换成相应的认证调用
					if (err) { logger.error("认证出错 "+obj.uid); req.session.destroy(); res.json({msg:"认证出错"}); addfail(now,clientip); }
					else {
						logger.info("认证成功 "+obj.uid);
						loginfaillist=loginfaillist.filter(itm=>(itm.ip!=req.ip));
						callfindUser(obj.uid, (err, user)=> { //替换成相应的查询用户属性调用
							req.session.user={ userid:obj.uid.toLowerCase(),username:user.sn };
							let clientip=req.header('x-forwarded-for')?req.header('x-forwarded-for'):req.ip;
							let sid=myencrypt(clientip+req.sessionID);
							res.setHeader('Set-Cookie', 'sid='+sid+';')
							let ticketid="t_"+moment().format("YYYYMMDDHHmmssSSS")+uuidv4().replace(/-/g, '');
							client.set(ticketid,JSON.stringify(req.session.user));
							res.json({msg:"pass",ticket:ticketid});
							})
						}
					});
				}
			});

		}

});

app.get('/queryticket',(req,res)=>{

	client.get(req.query.ticket).then(val=> {
		if (val!=null) {
			let jssouser=JSON.parse(val);
			client.del(req.query.ticket);
			res.json({msg:'found',ssouser:jssouser.userid});
			}
		else { res.json({msg:'not found'}); }
		})

});

app.get('/reqlogin',(req,res)=>{

	let clientip=req.header('x-forwarded-for')?req.header('x-forwarded-for'):req.ip;
	let reqid="l_"+moment().format("YYYYMMDDHHmmssSSS")+uuidv4().replace(/-/g, '');
	let loginkey=uuidv4().replace(/-/g, '');
	let iv=uuidv4().replace(/-/g, '');
	client.set(reqid+clientip,loginkey+iv).then(val=> {res.json({"reqid":reqid,"key":loginkey,"iv":iv}); });
	client.expire(reqid,5);

});

app.get('/logout',(req,res)=>{

	let redurl=(req.query.redurl!=undefined)?req.query.redurl:"/sso/index.html";
	logger.info("logout 页面重定向到 "+redurl);
	req.session.destroy( () => { res.redirect(redurl); } );

});

app.get('/sso',(req,res)=>{

	let ticketid=moment().format("YYYYMMDDHHmmssSSS")+uuidv4().replace(/-/g, '');
	client.set(ticketid,JSON.stringify(req.session.user));

	let urlParsed = new URL(req.query.redurl);
	urlParsed.searchParams.delete("ticket");
	urlParsed.searchParams.append("ticket",ticketid);
	let redurl=urlParsed.toString()

	logger.info("页面重定向到 "+redurl);
	res.redirect(redurl);
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值