实现一个最简单的单点登录(SSO)系统


一个最简单的单点登录(SSO)系统,包括认证中心(auth-server)、登录页(login-page)、资源服务端(resource-erver)和客户端(sso-client)四个部分,以下是详细的实现步骤。


1. 认证中心(Auth Server)

认证中心负责用户认证,并在成功认证后生成一个 JWT token。

安装依赖

mkdir auth-server
cd auth-server
npm init -y
npm install express jsonwebtoken

auth-server/index.js

const express = require('express')
const jwt = require('jsonwebtoken')
const app = express()
const PORT = 3001

app.use(express.json())

const users = {
  user1: 'password1',
  user2: 'password2',
}

const secretKey = 'your-secret-key' // 请使用环境变量或安全存储

const generateToken = (username) => {
  return jwt.sign({ username }, secretKey, { expiresIn: '1h' })
}

app.post('/login', (req, res) => {
  const { username, password } = req.body
  if (users[username] === password) {
    const token = generateToken(username)
    res.json({ success: true, token })
  } else {
    res.status(401).json({ success: false, message: 'Invalid credentials' })
  }
})

app.get('/verify', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (token) {
    try {
      const decoded = jwt.verify(token, secretKey)
      res.json({ success: true, user: decoded.username })
    } catch (error) {
      res.status(401).json({ success: false, message: 'Invalid token' })
    }
  } else {
    res.status(401).json({ success: false, message: 'No token found' })
  }
})

app.listen(PORT, () => {
  console.log(`Auth server listening on port ${PORT}`)
})

2. 登录页(React)

登录页负责用户输入用户名和密码,并发送请求到认证中心进行认证。

安装依赖

npx create-react-app login-page
cd login-page
npm install axios

login-page/src/App.js

import React, { useState } from 'react'
import axios from 'axios'

function App() {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const handleLogin = async () => {
    try {
      const response = await axios.post('http://localhost:3001/login', {
        username,
        password,
      })
      if (response.data.success) {
        localStorage.setItem('token', response.data.token)
        alert('Login successful')
        window.location.href = '/client' // 重定向到客户端
      } else {
        alert('Login failed')
      }
    } catch (error) {
      console.error('Login error:', error)
      alert('Login failed')
    }
  }

  return (
    <div>
      <h1>Login Page</h1>
      <div>
        <input type='text' placeholder='Username' value={username} onChange={(e) => setUsername(e.target.value)} />
        <input type='password' placeholder='Password' value={password} onChange={(e) => setPassword(e.target.value)} />
        <button onClick={handleLogin}>Login</button>
      </div>
    </div>
  )
}

export default App

3. 资源服务端(Resource Server)

资源服务端负责验证 JWT token,并提供受保护的资源。

安装依赖

mkdir resource-server
cd resource-server
npm init -y
npm install express jsonwebtoken

resource-server/index.js

const express = require('express')
const jwt = require('jsonwebtoken')
const app = express()
const PORT = 3002

app.use(express.json())

const secretKey = 'your-secret-key' // 请使用环境变量或安全存储

const validateToken = (token) => {
  try {
    const decoded = jwt.verify(token, secretKey)
    return decoded.username
  } catch (error) {
    return null
  }
}

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1]
  const user = validateToken(token)
  if (user) {
    res.json({ message: 'This is protected data', user })
  } else {
    res.status(401).json({ message: 'Unauthorized' })
  }
})

app.listen(PORT, () => {
  console.log(`Resource server listening on port ${PORT}`)
})

4. 客户端(React)

客户端负责展示受保护的资源,并处理用户登录状态。

安装依赖

npx create-react-app sso-client
cd sso-client
npm install axios

sso-client/src/App.js

import React, { useState, useEffect } from 'react'
import axios from 'axios'

function App() {
  const [protectedData, setProtectedData] = useState(null)

  useEffect(() => {
    const token = localStorage.getItem('token')
    if (token) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
    }
  }, [])

  const handleFetchProtectedData = async () => {
    try {
      const response = await axios.get('http://localhost:3002/protected')
      setProtectedData(response.data)
    } catch (error) {
      console.error('Fetch error:', error)
      alert('Failed to fetch protected data')
    }
  }

  return (
    <div>
      <h1>SSO Client</h1>
      <div>
        <button onClick={handleFetchProtectedData}>Fetch Protected Data</button>
        {protectedData && (
          <div>
            <h2>Protected Data</h2>
            <pre>{JSON.stringify(protectedData, null, 2)}</pre>
          </div>
        )}
      </div>
    </div>
  )
}

export default App

运行项目

1. 启动认证中心
cd auth-server
node index.js
2. 启动资源服务端
cd resource-server
node index.js
3. 启动登录页
cd login-page
npm start
4. 启动客户端
cd sso-client
npm start

测试

  1. 打开浏览器,访问 http://localhost:3000(登录页)。
  2. 输入用户名和密码(例如 user1password1),点击登录。
  3. 登录成功后,页面会重定向到 http://localhost:3003(客户端)。
  4. 在客户端页面,点击 Fetch Protected Data 按钮,查看受保护的数据。

总结

这个示例展示了如何使用 Node.js 和 Express 实现一个简单的单点登录系统,包括认证中心、登录页、资源服务端和客户端。认证中心负责用户认证并生成 JWT token,资源服务端验证 JWT token 并提供受保护的资源,登录页和客户端分别负责用户登录和展示受保护的资源。按照最常见的情况, token 存储在 localStorage 中,并在发送请求时包含在 HTTP 头中。希望这个示例能帮助你理解 SSO 的基本原理和实现方法。