聊聊基於Node實現單點登入(SSO)的方法

2022-12-06 22:05:29
什麼是單點登入?原理是什麼?怎麼實現?下面本篇文章帶大家瞭解一下單點登入,並聊聊使用實現單點登入SSO的方法,希望對大家有所幫助!

node.js極速入門課程:進入學習

【相關教學推薦:、】

什麼是單點登入

隨著公司業務的增多,必然會產生各個不同的系統,如果每個系統都需要單獨登入的話就會很不方便。

因此產生了單點登入這樣的解決方案,單點登入全稱 Single Sign On,簡稱SSO,意思是在多個系統應用群中登入一個系統,便可在其他所有系統中得到授權而無需再次登入。

比如小明今天登入了淘寶,如果沒有登入,就會被要求輸入認證資訊(使用者名稱密碼等),登入過後再去存取天貓的頁面時就不需要登入可以直接存取。

單點登入原理

SSO 需要有一個獨立的認證中心,只有獨立的驗證中心能接受使用者的使用者名稱密碼等安全資訊,其他系統不提供登入入口,只接受認證中心的間接授權。 整個過程可以簡單的用上圖描述:

  • 當使用者登入存取應用A時,應用A發現使用者未登入,跳轉至SSO認證中心,並將自己的地址作為引數方便回撥

  • SSO認證中心發現使用者沒有登入過,將使用者引導至登入頁面;使用者填寫使用者名稱密碼提交登入申請;SSO認證中心校驗使用者資訊,建立使用者雨SSO認證中心的對談(這時會把資訊儲存到cookie中),同時建立授權令牌token

  • sso認證中心帶著令牌跳轉到最初的請求地址(應用A)

  • 應用A拿到令牌去SSO認證中心認證是否有效,如果返回有效註冊應用A

  • 應用A建立與使用者之間的對談,展示資源並維持使用者登入態

  • 當使用者存取應用B時,發現使用者未登入(SSO認證伺服器與應用A應用B不是同一個域,不能提供登入態),跳轉到SSO認證中心,並將自己的地址和之前和SSO認證中心對談的cookie資訊帶入

  • SSO認證中心發現使用者已登入,跳轉回應用B地址,並附上令牌token

  • 同樣的應用B拿到令牌去SSO認證中心認證是否有效,如果返回有效註冊應用B

  • 應用B建立與使用者之間的對談,展示資源並維持使用者登入態

NodeJS 演示

三個不同的服務

這裡我們需要啟動三個服務來分別模擬 應用A,SSO認證伺服器和應用B

這裡埠號 8383的服務是SSO認證伺服器,其餘的 :8686 和 :8787 分別代表應用A與應用B。

其實應用A與應用B的程式碼幾乎一樣,如上圖所示我們可以通過穿參的方式來設定不同的埠及應用名。

先來看下效果

首次存取跳轉至登入頁

應用A判斷登入態,跳轉到SSO認證伺服器

應用A

const Koa=require('koa');
const Router=require('koa-router')
const views=require('koa-views')
const static=require('koa-static')
const path=require('path');
const app=new Koa();
const router=new Router();
const session=require('koa-session')
const koa2Req=require('koa2-request');

//模版引擎相關設定
app.use(views(path.join(__dirname,'./views')),{
    extension:'ejs'
  })
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))

const system=process.env.SERVER_NAME
router.get("/",async (ctx)=>{
    //通過 session來判斷 應用A的登入狀態
    let user=ctx.session.user
    if(user){
     //...
    }
    else //1、當使用者登入存取應用A時,應用A發現使用者未登入(應為伺服器沒有儲存對應的session)
    {
      let token=ctx.query.token
      //第一次登入url上也不會有令牌
      if(!token)
      {
      //1、跳轉到SSO認證伺服器
       ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
      }
      else
      {
        //...
      }
    }
})
app.use(router.routes())


const port=process.env.PORT||8888

app.listen(port,()=>{
    console.log(`app ${system} running at ${port}`)

})
登入後複製

認證伺服器判斷登入態,渲染登入頁

認證伺服器SSO

認證伺服器的目錄結構如下 主要處理兩個功能,一是登入邏輯,二是之後驗證令牌的有效性,分別有路由 login.js 和 check-token.js 處理

Auth/index.js

const Koa=require('koa');
const Router=require('koa-router')
const views=require('koa-views')
const path=require('path');
const app=new Koa();
const router=new Router();
const login=require("./routes/login")
const checkToken=require('./routes/check-token')
const bodyparser=require('koa-bodyparser')

app.use(views(path.join(__dirname,'./views')),{
    extension:'ejs'
  })
app.use(bodyparser())
//處理登入相關的邏輯
router.use('/login',login.routes())
//處理令牌驗證的邏輯
router.use('/check_token',checkToken.routes())
app.use(router.routes())

app.listen(8383,()=>{
    console.log(`app listen at 8383`)
})
登入後複製

剛才我們從應用A跳轉到 http://localhost:8383/login?redirectUrl=localhost:8686來看login中的邏輯
Auth/routes/login.js

const service = require("../service");
const router=require("koa-router")()


router.get('/',async (ctx)=>{
  const cookies=ctx.cookies;
  const token=cookies.get('token');
  //從cookie中判斷應用A的登入態
  if(token && service.isTokenVailid(token)){
    //。。。如果有登入過
  }else{
    //2、SSO認證中心發現使用者沒有登入過,於是渲染登入頁面登入頁面;
    await ctx.render('login.ejs',{
        extension:'ejs'
     })
  }
})

//。。。
module.exports=router
登入後複製

登入頁面

Auth/views/login.ejs

<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>統一登入</title>
</head>
<body>
    <h1>統一登入</h1>
    <form method="post">
       <div>使用者名稱: <input type="text" name="name"/></div>
       <div>密碼  <input type="text" name="password" /></div>
       <div><input type="submit" value='登入'></div>
    </form>
</body>
</html>
登入後複製

校驗使用者資訊,建立令牌

Auth/routes/login.js

router.post('/',async (ctx)=>{
//2、使用者填寫使用者名稱密碼提交登入申請;
   const body=ctx.request.body;
   const {name,password}=body;
    //2、SSO認證中心校驗使用者資訊,
   if(name==="admin" && password==="123456"){
    //2、建立使用者雨SSO認證中心的對談(這時會把資訊儲存到cookie中),同時建立授權令牌token
       const token="passport";
       await ctx.cookies.set('token',token,{
           maxAge:1000*60*60*24*30,
           httpOnly:true
       })
       if(ctx.query.redirectUrl){
       //3、sso認證中心帶著令牌跳轉到最初的請求地址(應用A)
           ctx.redirect(`${ctx.protocol}://${ctx.query.redirectUrl}?token=${token}`)
           //回跳地址是 http://localhost:8686/?token=passport
       }else{
           ctx.body="<h1>登入成功!</h1>"
       }
   }else{
       ctx.response.body={
           error:1,
           msg:'使用者名稱或密碼錯誤'
       }
   }
})
登入後複製

從認證伺服器攜帶令牌跳轉回應用A

令牌校驗 返回資源

應用A

app.use(views(path.join(__dirname,'./views')),{
    extension:'ejs'
  })

//...

const system=process.env.SERVER_NAME
router.get("/",async (ctx)=>{
    let user=ctx.session.user
    if(user){
      //...
    }
    else
    //這時應用A依舊沒有登入態 但url上有了令牌 http://localhost:8686/?token=passport
   {
      let token=ctx.query.token
      if(!token)
      {
        //...跳轉去SSO登入頁面
      }
      else 
      //跳回應用A時走這裡的邏輯
      {
        //ajax請求 4. 應用A拿到令牌去SSO認證中心認證是否有效,如果返回有效註冊應用A
        const url=`://localhost:8383/check_token?token=${token}&t=${new Date().getTime()}`
        let data = await koa2Req(ctx.protocol + url);
        if(data && data.body){
            try {
                const body=JSON.parse(data.body)
                const {error,userId}=body;
                // console.log(error,userId) 0,admin
                if(error==0){
                    if(!userId){
                        ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
                        return
                    }
                    //驗證通過後註冊session,渲染頁面
                    //5. 應用A建立與使用者之間的對談,展示資源並維持使用者登入態
                    ctx.session.user=userId;
                    await ctx.render('index.ejs',{
                        user:userId,
                        system
                    })
                }else{
                    ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
                }
            } catch (error) {console.log(error)}

            
        }
      }
    }
})
app.use(router.routes())

const port=process.env.PORT||8888

app.listen(port,()=>{
    console.log(`app ${system} running at ${port}`)

})
登入後複製

與之對應的 SSO中處理驗證令牌的邏輯
Auth/routes/check-token

const router=require("koa-router")()
const service=require("../service")

router.get('/',async (ctx)=>{
  const token=ctx.query.token;
  const result={
      error:1
  }
  //當token 是 password時
  if(service.isTokenVailid(token)){
    result.error=0;
    result.userId='admin'
  }
  ctx.body=result
 

})


module.exports=router
登入後複製

Auth/service/index.js

module.exports={
    isTokenVailid: function(token){
      if(token && token==='passport'){
          return true
      }
      return false
    }
}
登入後複製

至此使用者已經能正常存取應用A,SSO伺服器和應用A伺服器上都有了使用者登入過的資訊。

存取應用B

帶cookie跳轉至SSO認證伺服器

應用B

//...

router.get("/",async (ctx)=>{
    let user=ctx.session.user
    if(user){
      //...
    }else{
      let token=ctx.query.token
      //...
      if(!token)
      {
      //同樣既沒有session也沒有令牌,跳轉到SSO認證伺服器
      //6、當使用者存取應用B時,發現使用者未登入(SSO認證伺服器與應用A應用B不是同一個域,不能提供登入態),跳轉到SSO認證中心,並將自己的地址和之前和SSO認證中心對談的cookie資訊帶入
          ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
      }
      else
      {
        //。。。驗證令牌的部分
      }
    }
})
app.use(router.routes())

const port=process.env.PORT||8888

app.listen(port,()=>{
    console.log(`app ${system} running at ${port}`)

})
登入後複製

從認證伺服器攜帶令牌跳轉回應用B

SSO認證伺服器 ,再次登入時攜帶了cookie,因此不會再請求登入頁面 Auth/routes/login

//...
router.get('/',async (ctx)=>{
  const cookies=ctx.cookies;
  const token=cookies.get('token');
  //7. SSO認證中心發現使用者已登入,跳轉回應用B地址,並附上令牌token
  if(token && service.isTokenVailid(token)){
    const redirectUrl=ctx.query.redirectUrl;
    if(redirectUrl){
       //帶著令牌跳轉回應用B
        ctx.redirect(`${ctx.protocol}://${redirectUrl}?token=${token}`)
    }else{
        ctx.body="<h1>登入成功!</h1>"
    }
  }else{
    //...渲染登入頁面
  }
})
//..
登入後複製

令牌校驗 返回資源

這裡的邏輯和5,6兩步一樣,因為token容易偽造,所以要檢驗真偽。 應用B

app.use(views(path.join(__dirname,'./views')),{
    extension:'ejs'
  })

//...

const system=process.env.SERVER_NAME
router.get("/",async (ctx)=>{
    let user=ctx.session.user
    if(user){
      //...
    }
    else
    //這時應用B依舊沒有登入態 但url上有了令牌 http://localhost:8787/?token=passport
   {
      let token=ctx.query.token
      if(!token)
      {
        //...跳轉去SSO登入頁面
      }
      else 
      //跳回應用B時走這裡的邏輯
      {
        //ajax請求 8. 同樣的應用B拿到令牌去SSO認證中心認證是否有效,如果返回有效註冊應用B
        const url=`://localhost:8383/check_token?token=${token}&t=${new Date().getTime()}`
        let data = await koa2Req(ctx.protocol + url);
        if(data && data.body){
            try {
                const body=JSON.parse(data.body)
                const {error,userId}=body;
                // console.log(error,userId) 0,admin
                if(error==0){
                    if(!userId){
                        ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
                        return
                    }
                    //驗證通過後註冊session,渲染頁面
                    //9. 應用B建立與使用者之間的對談,展示資源並維持使用者登入態
                    ctx.session.user=userId;
                    await ctx.render('index.ejs',{
                        user:userId,
                        system
                    })
                }else{
                    ctx.redirect(`http://localhost:8383/login?redirectUrl=${ctx.host+ctx.originalUrl}`)
                }
            } catch (error) {console.log(error)}

            
        }
      }
    }
})
app.use(router.routes())

const port=process.env.PORT||8888

app.listen(port,()=>{
    console.log(`app ${system} running at ${port}`)

})
登入後複製

至此單點登入的大部分邏輯都已經完成,之後再session有效期內再存取頁面,就不需要再登入,直接返回資源

router.get("/",async (ctx)=>{
//如果session中有使用者資訊,說明已經登入過,直接返回請求資源
    let user=ctx.session.user
    if(user){
        await ctx.render('index.ejs',{
              user,
              system
        })
    }
    //...
 })
登入後複製

原文地址:https://juejin.cn/post/7088343138905325582

作者:YoYo君

更多node相關知識,請存取:!

以上就是聊聊基於Node實現單點登入(SSO)的方法的詳細內容,更多請關注TW511.COM其它相關文章!