什么是单点登录
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点登录流程
1、分别有单点系统app1,app2,单点认证服务器sso
2、用户访问app1,发现未登录,重定向到sso服务器并将自己的请求地址作为参数
3、sso引导用户登录,登录成功将登录凭证设置到cookie中,建立全局会话,并把token携带重定向到app1。
4、app1拿到token后请求sso校验token是否有效,有效则建立局部会话,至此app1登录成功.
5、用户访问app2系统,发现未登录于是重定向到sso认证中心,并将app2的地址作为请求参数
6、app2重定向到sso时因为sso已登录成功,所以有sso服务上的cookie,sso校验cookie并携带token重定向到app2
7、app2拿到token请求sso校验,生效后则建立app2的局部会话,至此app2登录成功。
nodejs实现
- sso代码
const Koa = require('koa');
const app = new Koa();
const path = require('path')
const static = require('koa-static');
const views = require('koa-views');
const json = require('koa-json')
const koaBody = require('koa-body');
const logger = require('koa-logger')
const session = require('koa-session')
const index = require('./src/router/router')
const CONFIG = {
key: 'koa.sess',
maxAge: 86400000,
autoCommit: true,
overwrite: true,
httpOnly: false,
signed: true,
rolling: false,
renew: false,
secure: false,
sameSite: null,
};
app.use(views(path.join(__dirname,'./static')),{
extension:'html'
})
// app.use(static(path.join(__dirname+'/static'), // 开放的文件夹 __dirname为当前运行文件的目录 目前看来 只能开放文件夹 无法开放单独一个文件
// {
// index:true, // 默认为true 访问的文件为index.html 可以修改为别的文件名或者false
// hidden:false, // 是否同意传输隐藏文件
// defer:true, // 如果为true,则在返回next()之后进行服务,从而允许后续中间件先进行响应
// }
// ))
app.keys = ['some secret hurr'];
app.use(session(CONFIG, app));
// middlewares
app.use(koaBody({
multipart: true, //解析多个文件
formidable: {
maxFileSize: 100 * 1024 * 1024, // 设置上传文件大小最大限制,默认2M
//uploadDir: 可以填写一个路径,不填写默认为 os.tmpDir()
}
}))
app.use(json())
app.use(index.routes(),index.allowedMethods())
// app.use()
app.use(logger())
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
// app.use(ctx => {
// // ignore favicon
// if (ctx.path === '/favicon.ico') return;
// let n = ctx.session.views || 0;
// ctx.session.views = ++n;
// ctx.body = n + ' views';
// });
app.listen(8383);
router代码
const Router = require('koa-router')
const router = new Router();
const {
v4: uuid
} = require('uuid');
router.get('/login',async (ctx, next) => {
console.log(ctx.cookies)
const token = ctx.cookies.get('token')
if(token){
ctx.redirect(`${ctx.query.redirectUrl}?token=${token}`)
return
}
await ctx.render('index.html')
})
router.post('/login', async (ctx, next) => {
console.log(ctx.querystring)
let {
username,
password,
origin
} = ctx.request.body
let userInfo = {
username: 'admin',
password: '123456'
}
if (username === userInfo.username && password === userInfo.password) {
let tk = uuid().replace(new RegExp("-", "g"), "").toLocaleLowerCase()
let token = uuid().replace(new RegExp("-", "g"), "").toLocaleLowerCase()
// ctx.tickets[tk] = userInfo.username
ctx.session[token] = tk
ctx.cookies.set('token', token, {
maxAge: 60 * 60 * 1000, //有效时间,单位毫秒
httpOnly: false,
path: '/',
});
ctx.redirect(`${ctx.query.redirectUrl}?token=${tk}`)
} else {
//todo 登录失败
console.log('登陆失败')
}
})
router.get('/check_token', async (ctx, next) => {
const token=ctx.query.token;
const result={
error:0,
userId:'admin'
}
ctx.body=result
})
module.exports = router
登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div>
欢迎登陆
<div>
用户名:<input type="text" id="name"/>
</div>
<div>
密码:<input type="text" id="password"/>
</div>
<button onclick="login()">登陆</button>
</div>
</body>
<script>
function login() {
var xhr = new XMLHttpRequest();
var data = {
username:document.getElementById('name').value,
password:document.getElementById('password').value
}
xhr.open('post', "/login"+window.location.search, true);
xhr.setRequestHeader('Content-Type','application/json;charset=UTF-8')
xhr.send(JSON.stringify(data));
xhr.onreadystatechange = function () {
// 这步为判断服务器是否正确响应
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
}
</script>
</html>
app1、app2代码
const Koa=require('koa');
const Router=require('koa-router')
const static=require('koa-static')
const path=require('path');
const app=new Koa();
const router=new Router();
const cors = require('koa2-cors');
const session=require('koa-session')
const koa2Req=require('koa2-request');
app.keys=['key']
const keyMap={
'8686':'koa:sess8686',
'8787':'koa:sess8787'
}
const CONFIG={
key:keyMap[process.env.PORT] || 'koa:sess',
maxAge:1000*60*60*24,
httpOnly:true
}
app.use(session(CONFIG,app))
app.use(cors({
// 任何地址都可以访问
origin:"*",
// 指定地址才可以访问
// origin: 'http://localhost:8080',
maxAge: 2592000,
// 必要配置
credentials: true
}));
const system=process.env.SERVER_NAME
router.get("/",async (ctx)=>{
let user=ctx.session.user
console.log(`user in session`,ctx.session)
if(user){
ctx.body = '登录成功'
}else{
let token=ctx.query.token
if(!token)//第一次不带token进来需要去登录页面
{
ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.origin}`)
}
else //第二次进入的时候走这一步 发请求给认证服务器验证token
{
//ajax请求
const url=`://localhost:8383/check_token?token=${token}&t=${new Date().getTime()}`
let data = await koa2Req(ctx.protocol + url);
console.log(data.body)
if(data && data.body){
try {
const body=JSON.parse(data.body)
const {error,userId}=body;
console.log(error,userId)
if(error==0){
if(!userId){
ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.origin}`)
return
}
ctx.session.user=userId;
ctx.body ='登录成功'
}else{
ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.origin}`)
}
} catch (error) {
// ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
}
}
}
}
})
app.use(router.routes())
const port=process.env.PORT||8787
app.listen(port,()=>{
console.log(`app ${system} running at ${port}`)
})