nodejs手撸简易版 CAS 中央认证服务

Posted on 2020-12-09 00:17:22
Author: 可乐小可爱メ
1. 背景

公司各种项目,但是又是同一维度(张)用户表..  基于java 使用CAS 中央认证..


2. 技术选型

2.1 原本觉得很简单的一个逻辑, 各种资料搜搜搜 比如:

    2.1.1 CAS(中央认证服务)_百度百科

    2.1.2 connect-cas2 - npm

    2.1.3 基于node.js的sso(单点登录-客户端校验)

 实在太多了, 但是又都是封装好的, 还是自己手撸一个(低配版) 简单实现先..

3. 时序逻辑图



3.1 说明

    3.1.1 实线 虚头 代表 不带状态访问/跳转

    3.1.2 实现 实头 代表 带状态跳转

    3.1.3 虚线 代表 ajax 异步请求

4. 实现思路

4.1 核心部分: 

    4.1.1 所有应用鉴权基于 本地缓存的 token, 没有token时 统一重定向到 CAS 中心的 login页面,

    4.1.2 login页面 逻辑执行: 

        4.1.2  a: 获取cookie  TIANKELE_CAS 若无 写入cookie, 执行登录逻辑, 成功后 写入redis 签出 重定向回 项目页面 带上ticket

                 b:  获取cookie  TIANKELE_CAS 若存在 redis 获取对应的 ticket (重新签) 重定向回 项目页面 带上ticket

    4.1.3 项目页面 获取 ticket , ajax 用 ticket 换 token

    

5. 核心代码

5.1  CAS 部分

    5.1.1 cas/login 静态认证页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Central Authentication Service(CAS)</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
<script src="https://lib.baomitu.com/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<div class="jumbotron">
<h1 class="display-4">Central Authentication Service(CAS)</h1>
</div>
<div class="container">
<div class="input-group">
<span>账号:</span>
<input type="text" class="input-group" id="account" />
</div>
<div class="input-group">
<span>密码: </span>
<input type="password" class="input-group" id="password" />
</div>
<div class="input-group">
<span>验证码: </span>
<input type="text" class="input-group" />
</div>
<button type="button" class="btn btn-primary mt-4 submit">Submit</button>
</div>
</body>

<script>
function getQueryVariable(variable) {
var str = window.location.search.split("?");
var query = str[1];
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
return false;
}
console.log();
$(".submit").on("click", () => {
const account = $("#account").val();
const password = $("#password").val();
if (account && password) {
$.post("/login", { account, password }, (res) => {
if (res && res.code === 200 && res.ticket) {
if (!window.location.search) {
return false;
}
window.location.href =
getQueryVariable("service") + "?ticket=" + res.ticket;
} else {
alert(res.msg);
}
});
}
});
</script>
</html>

    

    5.1.2 cas/casLogin (API接口)

  const md5 = require("md5");

const uuidv4 = require("uuid");
const redisCli = require("./redis");
const fs = require("fs");

module.exports = (req, res) => {
const { cookies } = req;
const { query } = req
const service = query.service
const loginPage = fs.readFileSync("./statics/login.html");

// 第一次访问
if (!cookies || !cookies.TIANKELE_CAS) {
res.cookie("TIANKELE_CAS", md5(uuidv4()), {
maxAge: 60000 * 60 * 24,
httpOnly: true,
});
res.setHeader("Content-Type", "text/html;charset=utf-8");
return res.send(loginPage);
} else {
// 查看cookie 是否登录
redisCli.get(cookies.TIANKELE_CAS, async (err, result) => {
if (err || !result) {
res.setHeader("Content-Type", "text/html;charset=utf-8");
return res.send(loginPage);
} else {
// 已经登录
const { ticket } = JSON.parse(result)
return res.redirect(`${service}?ticket=${ticket}`)
}
});
}
};


    5.1.3 cas/ticket(API接口)  验证返回token

const releaseToken = require("./auth/releaseToken");
const createToken = require("./auth/createToken");
const { secret } = require("./auth/secret");
const redisCli = require("./redis");
const log = require("./logger");

module.exports = (req, res) => {
const { ticket } = req.body;
if (!ticket) {
return res.send({ code: 401, msg: "no ticket" });
}
const releaseTk = releaseToken(ticket);
const now = new Date().getTime();
const time = releaseTk.split(",")[0];
const account = releaseTk.split(",")[1];

if (now - time > 60000) {
return res.send({ code: 401, msg: "ticket expire" });
}
redisCli.get(account, (err, result) => {
if (err) {
log("service", { loc: "ticket check redis err", err });
return res.send({ code: 401 });
}
const token = createToken(secret + account + "," + new Date().getTime());
return res.send({ code: 200, data: { token } });
});
};


5.2 子项目部分(基于umi 简单处理)

    5.2.1 主页(模拟无状态重定向)

import React, { useEffect } from "react";
function Index() {
useEffect(() => {
if (!window.localStorage.token) {
alert("请登录!");
const { origin } = window.location;
window.location.href = `http://192.168.0.104:10001/cas/login?service=${origin}/token`;
}
}, [window.localStorage]);
return <div>Welcome to CAS project 10000</div>;
}
export default Index;


    5.2.2 token页面 (执行ticket换token)

import React, { useEffect } from "react";
import { useLocation, history } from "umi";
import request from "umi-request";

function Token() {
const {
query: { ticket },
} = useLocation();
useEffect(() => {
if (!ticket) {
history.replace("/signin");
}
request.post("/cas/token", { data: { ticket } }).then(response => {
if (response.code === 200 && response.data) {
const { token } = response.data;
if (token) {
window.localStorage.setItem("token", token);
history.replace("/")
}
}
});
}, [ticket]);
return <div>requesting...</div>;
}

export default Token;

 

6. 注意点 及 说明    

6.1 这里只是简单模拟, 实际接入 是在 全局做状态拦截, 若无token, 重定向到 cas/login

6.2 这里的cas 只负责 验证ticket (注意实际项目 需要一次性处理ticket 并且加入 ticket有效白名单域名), 加解密token

6.3 缺点:  只是模拟实现 中央认证功能, 缺陷和很多国内自行模拟实现的一样, cookie不能共享, 亦无法设置过期时间。

当前评论 (1) 登录后评论

可乐小可爱メ
2020-12-09 00:19:07

后续会更改成, 跨站共享cookie(统一cookie 统一指向redis 统一处理过期时间)

回复
1