本文目录导读:
前端视角下的单点登录实现方案
单点登录(SSO)原理概述
单点登录是一种身份验证机制,它允许用户使用一组凭据(如用户名和密码)登录到多个相关但独立的应用程序中,其核心原理在于建立一个信任的中央认证中心(Central Authentication Server,CAS)。
当用户首次访问应用程序A时,应用程序A检测到用户未登录,会将用户重定向到CAS,CAS提供一个登录界面,用户在此输入凭据进行登录,CAS验证凭据的有效性,如果验证成功,CAS会生成一个票据(Ticket),这个票据包含了用户的身份信息等相关数据,CAS将用户重定向回应用程序A,并将票据作为参数传递给应用程序A,应用程序A接收到票据后,会与CAS进行验证,确认票据的真实性和有效性,从而确定用户的登录状态。
图片来源于网络,如有侵权联系删除
当用户接着访问应用程序B时,应用程序B发现用户未登录,同样重定向到CAS,由于用户已经在CAS登录过,CAS会识别出用户已经登录的状态,直接为应用程序B生成一个新的票据,然后将用户重定向回应用程序B,应用程序B验证票据后完成登录过程,这样用户就无需再次输入凭据。
前端实现单点登录的关键技术与步骤
(一)跨域处理
在单点登录场景中,不同的应用程序可能位于不同的域名下,这就涉及到跨域问题,前端可以采用JSONP(JSON with Padding)或者CORS(Cross - Origin Resource Sharing)技术来解决跨域资源请求问题。
1、JSONP
- JSONP是一种利用<script>
标签没有跨域限制的特性来实现跨域数据传输的方法,在与CAS通信时,如果CAS支持JSONP,可以通过动态创建<script>
标签并设置其src
属性为CAS提供的包含票据数据的URL(以回调函数包裹数据的形式)。
```javascript
function handleTicket(data) {
// 处理从CAS获取的票据数据
}
var script = document.createElement('script');
script.src = 'https://cas.example.com/getTicket?callback=handleTicket';
document.body.appendChild(script);
```
- 不过,JSONP只能用于GET请求,存在一定的局限性。
2、CORS
- CORS是一种更为现代和灵活的跨域解决方案,在应用程序的服务器端需要设置正确的CORS头信息,例如在Node.js环境下使用Express框架,可以这样设置:
```javascript
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access - Control - Allow - Origin', '*');
res.header('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
res.header('Access - Control - Allow - Headers', 'Content - Type');
next();
});
```
- 在前端,当使用XMLHttpRequest或者fetch API向CAS发送请求时,就可以正常进行跨域请求,
```javascript
fetch('https://cas.example.com/getTicket', {
method: 'GET',
headers: {
'Content - Type': 'application/json'
}
}).then(response => response.json()).then(data => {
// 处理从CAS获取的票据数据
});
```
(二)票据存储与管理
1、Cookie存储
- 当应用程序从CAS获取到票据后,可以将票据存储在Cookie中,在JavaScript中,可以使用document.cookie
来操作Cookie。
```javascript
document.cookie = "ticket=" + ticketValue + "; path=/; expires=" + expirationDate.toUTCString();
```
- 需要注意Cookie的安全性,可以设置HttpOnly
属性来防止JavaScript脚本访问Cookie,从而避免XSS(跨站脚本攻击)窃取票据,设置合适的Secure
属性,确保Cookie只在HTTPS连接下传输,提高安全性。
2、LocalStorage或SessionStorage
- 除了Cookie,还可以将票据存储在LocalStorage或SessionStorage中,LocalStorage是一种持久化的本地存储,数据在浏览器关闭后仍然存在,除非被手动清除,SessionStorage则是会话级别的存储,当浏览器标签页或会话关闭时,数据就会被清除。
- 使用LocalStorage存储票据的示例:
```javascript
localStorage.setItem('ticket', ticketValue);
```
- 在后续的请求中,可以从LocalStorage或SessionStorage中取出票据,然后附加到请求中发送给应用程序服务器进行验证,在发送AJAX请求时:
```javascript
var ticket = localStorage.getItem('ticket');
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://app.example.com/api/validateTicket');
xhr.setRequestHeader('Content - Type', 'application/json');
xhr.send(JSON.stringify({ticket: ticket}));
```
(三)与CAS的交互逻辑
1、登录重定向
- 当应用程序检测到用户未登录时,需要将用户重定向到CAS的登录页面,在前端,可以使用window.location.href
来实现重定向。
```javascript
window.location.href = 'https://cas.example.com/login?redirect_uri=' + encodeURIComponent(window.location.href);
```
- 这里的redirect_uri
参数用于告诉CAS登录成功后将用户重定向回的地址。
2、票据验证
- 在应用程序接收到CAS返回的票据后,需要与CAS进行验证,这可以通过向应用程序服务器发送一个包含票据的请求,然后由应用程序服务器与CAS进行通信验证票据,在前端,需要构建正确的请求并处理验证结果。
- 使用fetch API发送验证请求:
```javascript
var ticket = getTicketFromCookieOrStorage();
fetch('https://app.example.com/api/verifyTicket', {
method: 'POST',
headers: {
'Content - Type': 'application/json'
},
body: JSON.stringify({ticket: ticket})
}).then(response => {
if (response.ok) {
// 验证成功,设置用户登录状态
setUserLoggedIn();
} else {
// 验证失败,可能需要重新登录
window.location.href = 'https://cas.example.com/login?redirect_uri=' + encodeURIComponent(window.location.href);
}
});
```
(四)用户登录状态的维护与同步
1、登录状态检测
- 在应用程序的每个页面加载时,前端需要检测用户的登录状态,可以通过检查是否存在有效的票据(从Cookie或LocalStorage/SessionStorage中获取)来判断,如果存在有效票据,可以认为用户已经登录,然后可以根据用户的身份信息进行页面内容的渲染,例如显示用户的用户名、个人信息等。
- 在页面加载时:
```javascript
document.addEventListener('DOMContentLoaded', function () {
var ticket = getTicketFromCookieOrStorage();
if (ticket) {
图片来源于网络,如有侵权联系删除
// 发送请求获取用户信息并渲染页面
fetch('https://app.example.com/api/getUserInfo', {
method: 'GET',
headers: {
'ticket': ticket
}
}).then(response => response.json()).then(userInfo => {
renderUserInfo(userInfo);
});
} else {
// 用户未登录,显示登录或注册链接
showLoginOrRegisterLinks();
}
});
```
2、状态同步
- 当用户在一个应用程序中进行登出操作时,需要通知CAS以及其他相关的应用程序,在前端,可以向应用程序服务器发送登出请求,应用程序服务器再与CAS通信进行全局登出,前端需要清除存储的票据(从Cookie或LocalStorage/SessionStorage中删除),并且更新用户的登录状态显示。
- 登出操作的实现:
```javascript
function logout() {
// 向应用程序服务器发送登出请求
fetch('https://app.example.com/api/logout', {
method: 'POST'
}).then(() => {
// 清除票据
clearTicketFromCookieOrStorage();
// 重定向到CAS的登出页面或者应用程序的未登录页面
window.location.href = 'https://cas.example.com/logout?redirect_uri=' + encodeURIComponent('https://app.example.com/');
});
}
```
单点登录前端实现的安全性考虑
(一)防止票据泄露
1、加密传输
- 在与CAS进行通信以及在应用程序之间传递票据时,应该使用加密的连接(如HTTPS),这可以防止票据在网络传输过程中被窃取,对于存储在本地的票据(无论是Cookie还是LocalStorage/SessionStorage),可以考虑对其进行加密存储,可以使用Web Crypto API在浏览器端对票据进行加密后再存储,在使用时再解密。
- 在JavaScript中使用Web Crypto API进行AES加密的示例:
```javascript
async function encryptTicket(ticket) {
const key = await window.crypto.subtle.generateKey(
{
name: 'AES - GCM',
length: 256
},
true,
['encrypt', 'decrypt']
);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES - GCM',
iv: iv
},
key,
new TextEncoder().encode(ticket)
);
const encryptedArray = new Uint8Array(encrypted);
const combined = new Uint8Array(iv.length + encryptedArray.length);
combined.set(iv);
combined.set(encryptedArray, iv.length);
return btoa(String.fromCharCode.apply(null, combined));
}
```
2、严格的访问控制
- 确保只有合法的前端代码能够访问票据,如前面提到的,设置Cookie的HttpOnly
属性可以防止JavaScript脚本访问Cookie中的票据,减少XSS攻击导致票据泄露的风险,对于LocalStorage和SessionStorage,要避免在代码中出现不必要的全局变量暴露,防止恶意脚本通过全局变量访问存储的票据。
(二)防范跨站请求伪造(CSRF)
1、CSRF令牌
- 在与CAS以及应用程序服务器进行交互时,使用CSRF令牌,当应用程序向CAS发送登录或验证请求时,除了票据之外,还需要包含一个CSRF令牌,这个CSRF令牌可以由应用程序服务器在用户首次访问页面时生成并发送给前端,然后前端在后续的请求中携带这个令牌。
- 在前端,可以将CSRF令牌存储在一个隐藏的表单字段或者作为请求头的一部分发送,在HTML表单中:
```html
<form id="loginForm" action="https://cas.example.com/login" method="post">
<input type="hidden" name="csrf_token" id="csrf_token" value="generated_token">
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<input type="submit" value="Login">
</form>
```
- 在使用fetch API发送请求时,可以将CSRF令牌作为请求头发送:
```javascript
var csrfToken = document.getElementById('csrf_token').value;
fetch('https://app.example.com/api/verifyTicket', {
method: 'POST',
headers: {
'Content - Type': 'application/json',
'X - CSRF - Token': csrfToken
},
body: JSON.stringify({ticket: ticket})
}).then(response => {
// 处理响应
});
```
单点登录前端实现的用户体验优化
(一)减少重定向次数
1、预取票据
- 在应用程序启动时或者在用户进行某些操作(如点击登录链接之前),可以预先向CAS发送请求获取票据,这样,当用户真正进行登录操作或者访问需要登录的资源时,可以减少重定向到CAS的等待时间,可以使用异步的fetch API在后台预取票据:
```javascript
async function prefetchTicket() {
图片来源于网络,如有侵权联系删除
try {
const response = await fetch('https://cas.example.com/prefetchTicket');
if (response.ok) {
const ticket = await response.json();
storeTicket(ticket);
}
} catch (error) {
// 处理预取票据失败的情况
}
}
prefetchTicket();
```
2、本地缓存策略
- 对于已经验证过的票据,可以在本地进行一定时间的缓存,如果用户在短时间内再次访问同一个应用程序或者相关应用程序,可以直接使用本地缓存的票据进行验证,而无需再次重定向到CAS,不过,需要注意缓存的有效性和安全性,定期更新缓存或者在CAS通知票据失效时及时清除缓存。
(二)统一的登录界面设计
1、样式和交互一致性
- 如果多个应用程序都采用单点登录,应该设计一个统一的登录界面风格,这包括颜色、布局、输入框样式等方面的一致性,在前端,可以使用CSS框架(如Bootstrap或Tailwind CSS)来实现统一的样式,在交互方面,如登录按钮的点击效果、错误提示信息的显示方式等也应该保持一致。
- 使用Bootstrap创建一个简单的统一登录界面:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device-width, initial - scale = 1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<title>统一登录界面</title>
</head>
<body>
<div class="container">
<div class="row justify - content - center">
<div class="col - md - 6">
<form id="loginForm">
<h2 class="text - center">登录</h2>
<div class="form - group">
<input type="text" class="form - control" id="username" placeholder="用户名">
</div>
<div class="form - group">
<input type="password" class="form - control" id="password" placeholder="密码">
</div>
<button type="submit" class="btn btn - primary btn - block">登录</button>
</form>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery - 3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/ppr.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
```
2、多语言支持
- 考虑到用户的多样性,统一的登录界面应该支持多语言,在前端,可以使用JavaScript的国际化库(如i18next)来实现多语言支持,定义不同语言的翻译文件,
- 英文(en.json):
```json
{
"login": "Login",
"username": "Username",
"password": "Password"
}
```
- 中文(zh.json):
```json
{
"login": "登录",
"username": "用户名",
"password": "密码"
}
```
- 然后在JavaScript中初始化i18next并加载相应的语言文件:
```javascript
import i18next from 'i18next';
import XHR from 'i18next - xhr - backend';
i18next
.use(XHR)
.init({
lng: 'en',
fallbackLng: 'en',
debug: true,
backend: {
loadPath: '/locales/{{lng}}.json'
}
});
document.addEventListener('DOMContentLoaded', function () {
var usernameInput = document.getElementById('username');
usernameInput.placeholder = i18next.t('username');
var passwordInput = document.getElementById('password');
passwordInput.placeholder = i18next.t('password');
var loginButton = document.getElementById('loginButton');
loginButton.textContent = i18next.t('login');
});
```
前端在单点登录实现中扮演着重要的角色,通过合理运用跨域处理技术、票据存储与管理方法、与CAS的交互逻辑以及考虑安全性和用户体验优化等方面,可以构建出一个高效、安全且用户友好的单点登录前端实现方案。
评论列表