流程说明
这篇教程我们将使用Express.js来实现单点登录系统,首先我们有两个域名(可在host中配置),分别是:
node.com
login.com
整个流程如上图所示:
- 如果用户访问
node.com
,服务端发现没有登录后自动重定向到login.com/login
页面 - 在
login.com/login
页面登录成功后,将带上有用户身份的jwt标准的token,跳转到node.com
node.com
解析token,如果解析成功,则注入cookie允许访问。
示例代码
1. 没有cookie和token时进行重定向(后端)
下面假设用户现在访问的是node.com
,现在请求上即没有cookie
,url上也没有携带token
。说明该用户没有登录,此时我们将请求重定向到login.com:3099/login
页面上。
需要注意的是,我们通过req
对象获取到了原始的域名,并经过encodeURIComponent()
后保存到了source
的参数上,方便之后在登录成功后进行跳转回原页面操作。
// 检查用户是否登录
app.get("/", async function (req, res) {
const url = getURL(req);
// 即没有cookie也没有token的情况
if (!req.cookies.USER_ID && !req.query.token) {
res.redirect(
`http://login.com:3099/login?source=${encodeURIComponent(url)}`
);
return
}
});
复制代码
2. 登录页面(前端)
此时页面跳转到了登录页页面,触发了/login
的路由,我们返回了一个登录的静态页面。
app.get("/login", async function (req, res) {
const loginPagePath = path.resolve(__dirname, "./views/login.html");
fs.readFile(loginPagePath, (err, html) => {
if (err) {
res.send("登录页读取错误");
} else {
res.set("Content-Type", "text/html");
res.send(html);
}
});
});
复制代码
页面比较简单,主要是为了实现一个简单的登录,登录页面的相关代码和样式如下。
<div id="login-page">
<input type="text" placeholder="用户名" v-model="name" />
<input type="password" placeholder="密码" v-model="pwd" />
<button @click="login">登录</button>
</div>
<script>
const LoginPage = {
data() {
return {
name: "",
pwd: "",
};
},
methods: {
login() {
const { name, pwd } = this;
fetch("/login", {
method: "POST",
redirect: 'follow',
mode: 'cors',
headers: new Headers({
"Content-Type": "application/json",
}),
body: JSON.stringify({ name, pwd }),
}).then(res => res.json()).then(data => {
location.replace(data.redirectURL)
});
},
},
};
Vue.createApp(LoginPage).mount("#login-page");
</script>
复制代码
3. 登录成功生成JWT(后端)
这里我们使用了jsonwebtoken
这个npm包来生成JWT为,它提供了jwt.sign(payload, secretKey, option)
方法来生成JWT。
const SECRET_KEY = "sSjXx81KjQiayUhHHlCRr9mU";
const jwt = require("jsonwebtoken");
app.post("/login", async function (req, res) {
const { name, pwd } = req.body;
const isValid = await validateUserLogin(name, pwd); // 验证用户登录
const query = url.parse(req.headers.referer).query; // 获取到请求的query
const sourceURL = decodeURIComponent(qs.parse(query).source); // 获取要跳转的地址
if (isValid) {
const token = jwt.sign({name}, SECRET_KEY, { expiresIn: '1h' })
res.cookie("USER_ID", name, {
httpOnly: true,
maxAge: ONE_HOUR,
});
res.header('Access-Control-Allow-Origin', '*')
const redirectURL = `${sourceURL}?token=${token}`
res.send({
success: true,
redirectURL: redirectURL
})
} else {
res.send({
success: false,
info: "登录失败",
});
}
});
复制代码
上面的代码做了如下事情:
- 获取到请求体里面的
name
和pwd
,验证用户身份。 - 获取到网站链接上面的
source
参数,用作登录成功后的重定向地址redirectURL
- 如果用户身份验证成功,调用
jwt.sign()
生成token
。 - 将
token
拼接在重定向的链接上,返回给前端,用来重定向。 - 此时页面跳转回
node.com
,整个页面链接是:[http://node.com:3099/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWJjIiwiaWF0IjoxNjMyNDkzMDM5LCJleHAiOjE2MzI0OTY2Mzl9.kaiOwYX9P6-v3WbryxyK8jJBseza3ZbEi5a56P28knA](http://node.com:3099/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWJjIiwiaWF0IjoxNjMyNDkzMDM5LCJleHAiOjE2MzI0OTY2Mzl9.kaiOwYX9P6-v3WbryxyK8jJBseza3ZbEi5a56P28knA)
4. 解析token
现在页面跳转回node.com
,我们需要解析token
。此时需要使用到jwt.verify()
方法。如果token
被篡改了,此时该方法会抛出错误。如果解析成功,在上一步我们的payload
中的{name}
对象能够够被我们成功获取到。让我们来给/
路由新增一个判断条件,用来解析token
// 检查用户是否登录
app.get("/", async function (req, res) {
const url = getURL(req);
// 有token的情况
if(req.query.token) {
const token = req.query.token
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
res.send({
success: false,
info: 'token无效'
})
} else {
res.cookie('USER_ID', decoded.name)
res.redirect('/')
}
})
return
}
// 即没有cookie也没有token的情况
if (!req.cookies.USER_ID && !req.query.token) {
res.redirect(
`http://login.com:3099/login?source=${encodeURIComponent(url)}`
);
return
}
});
复制代码
可以看到,如果解析成功后,我们将payload
中的name
的值保存到了cookie中。并进行了一次重定向,用来抹掉连接上冗余的token
字段,现在让我们再来添加一个提示用户登录成功的响应吧。
app.get("/", async function (req, res) {
const url = getURL(req);
// 有cookie的情况
if(req.cookies.USER_ID) {
res.send({
success: true,
info: `${req.cookies.USER_ID}已登录!`
})
return
}
// 有token的情况
if(req.query.token) {
const token = req.query.token
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
res.send({
success: false,
info: 'token无效'
})
} else {
res.cookie('USER_ID', decoded.name)
res.redirect('/')
}
})
return
}
// 即没有cookie也没有token的情况
if (!req.cookies.USER_ID && !req.query.token) {
res.redirect(
`http://login.com:3099/login?source=${encodeURIComponent(url)}`
);
return
}
});
复制代码
此时你会看到我们已经成功的给用户保存上了登录态:
近期评论