原文链接
前言
构建一个鉴权系统是简单的事情。但是对于文档管理平台,如何将各种形式文档转换成HTML
的形式,以及对于不同转换元素样式的调整,甚至支持UML/Mermaid图、文档版本管理等等,这些的工作量如果单人完成可以说是巨大的。所以我们我们这里需要一个工具,将简单的Markdown
文档变为一个可控的文档管理平台。在前文制作在线Markdown文档转Html以及Pdf工具、Marked.js渲染下md内图片点击放大解决方案等文章都有使用各种工具做过类似的操作,可以在Markdown转换看到效果,在文档较少或者没有成体系地对外/内预览需求,那么确实可以如此处理。但是仅仅这些是无法作为文档管理平台的具体解决方案的
鉴权系统
对于鉴权系统其实没什么可谈的,无非就时用户表,权限表,角色表啥等,然后根据后台配置在网关侧或者服务侧做相应接口过滤,我们这里就不聊业务的处理部分了,总之我们认为目前已经存在了一个系统,提供登录、鉴权相关功能,并且登录页和Writerside
处于同一个域名下。当然如果已经有了登录页,希望做单点登录也不是什么难事,做个公共的认证中心页面即可,或者直接改动之前登录页面作为公共认证中心
Writerside
这里我们仍然使用Jetbrains
全家桶中的Writerside,没有证书的小伙伴参考免费获得IDEA证书,关于如何使用这个软件工作,官方文档解释地很清楚了,这里没有必要重复,我们这里主要聊以下文档中没有提及的或者一笔带过但是比较重要的东西
自定义页脚
我们注意到使用Writerside
生成文档管理平台后,包括logo
、favicons
、实例标题都可以自定义,但是目前版本2024.1.0
页面展示上几乎算唯一不可定义的东西就是页脚的Powered by JetBrains Writerside
。我们这里需要稍微使用一些奇技淫巧,文档中提到可以使用<include-in-head>
来增加一些可以自定义的头标签,我们可以在头标签中复写样式,来达到隐藏页脚的目的
最后构建buildprofiles.xml
如下:
<?xml version="1.0" encoding="UTF-8"?>
<buildprofiles xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/build-profiles.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<build-profile instance="ad">
<variables>
<!--else-->
<include-in-head>patch.html</include-in-head>
<!--else-->
</variables>
<footer>
<copyright>2020-2024 astercasc.com
| 互联网ICP备案:浙ICP备2022023127号
</copyright>
</footer>
</buildprofiles>
patch.html
:
<style>
.footer__powered {
display: none;
}
.footer__wrapper {
padding-right: 0;
display: flex;
justify-content: center;
}
.wh-header__link {
display: flex;
align-items:center;
}
.wh-header__product-logo {
height: 36px;
}
</style>
这里我稍微调整了以下logo
部分的样式,小伙伴也可以根据自己的喜好覆盖样式。稍微提一嘴,目前版本的<footer>
内的标签极其难用,基本上没有好用的,除了样式有问题就是本身就有问题,《点名表扬》<icp>
,说是为了中国大陆开发的,然后发现中国大陆就一个上海
添加请求头
如果我们需要对文档进行鉴权就要了解当前用户,就需要根据token
来区分用户并且在后端进行鉴权返回结果。这个token
将在用户登录的时候被存入localStorage
,所以我们需要取出并附在所有静态资源的请求中,那么我们应该如何在Writerside
项目中实现这个需求呢,还是利用<include-in-head>
,在patch.html
中添加:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(function (registration) {
let curToken = localStorage.getItem('User-Token')
if (registration.active) {
if(!curToken) {
curToken = ''
}
registration.active.postMessage({
type: 'SET_USER_TOKEN',
userToken: curToken
});
}
}).catch(function (error) {
console.log('Service Worker registration failed:', error);
});
}
</script>
我们这里使用serviceWorker
完成该功能,将service-worker.js
注册完成后将token
发送过去,完成静态资源请求头的携带,service-worker.js
如下:
let userToken = '';
self.addEventListener('install', function(event) {
self.skipWaiting();
});
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', function(event) {
event.respondWith(
(async function() {
const originalHeaders = new Headers(event.request.headers);
if(userToken) {
originalHeaders.set('User-Token', userToken);
}
const modifiedRequest = new Request(event.request, {
headers: originalHeaders,
});
try {
const response = await fetch(modifiedRequest);
return response;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
})()
);
});
self.addEventListener('message', function(event) {
if (event.data && event.data.type === 'SET_USER_TOKEN') {
userToken = event.data.userToken || '';
}
});
导航栏触发鉴权操作
我们会发现在进行静态资源的请求后,如果用户没有登录或者没有权限从而无法请求到资源时,如果只是重定向那么使用nginx
即可解决。但有些小伙伴可能需要一些更人性化的提示或者个性化的操作,这里的实现方式也是和上面一样,在<include-in-head>
中<script>
添加:
function yourMatchRule() {
//...
}
function sendHttpRequestWithXHR(init = false) {
if (init && !yourMatchRule()) {
return;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://domin.com/exampleauthcheck', true);
let curToken = localStorage.getItem('User-Token')
if (curToken) {
xhr.setRequestHeader('User-Token', localStorage.getItem('User-Token'));
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 401) {
window.location.pathname = '/login';
//console.log("error")
} else {
//console.log("success")
}
}
};
xhr.send();
}
window.addEventListener('load', () => {
let tocList = document.getElementsByClassName("toc")
if (tocList && tocList.length > 0) {
let toc = tocList[0];
toc.addEventListener('click', function (event) {
if (event.target.tagName === 'A') {
if (yourMatchRule()) {
sendHttpRequestWithXHR()
}
}
});
}
});
我们这里为右侧导航栏添加点击事件,当请求页面满足某种个性化配置时候,触发接口鉴权操作,如果结果状态为401
则返回登录页
反向代理配置
这里以Nginx
举例:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name doc.astercasc.com;
#some base setting
location ~ (your_match_regular) {
auth_request /auth;
error_page 401 = @error401;
root /examplePath/doc.astercasc.com;
try_files $uri =404;
}
location = /auth {
internal;
set $auth_request_url $scheme://$host$request_uri;
proxy_pass http://localhost:9527/exampleauthcheck?url=$auth_request_url;
#some base setting
}
location /documents {
root /examplePath/doc.astercasc.com/;
try_files $uri $uri/ /starter.html;
}
location = /documents/ {
return 301 /documents/starter.html;
}
location /service-worker.js {
root /examplePath/doc.astercasc.com/work/;
}
location / {
root /examplePath/doc.astercasc.com/frame;
try_files $uri $uri/ /index.html =404;
}
location @error401 {
return 301 /login;
}
#some base setting
}
这里假设doc.astercasc.com
下有三个文件夹frame
、documents
、work
分别放置基础项目,文档管理平台、脚本。首先定义一个需要鉴权的匹配正则,如果你这里全部需要鉴权,则直接使用Writerside
发布的所在文件夹即可。鉴权成功直接允许对于该文件的访问。当用户没有权限时,则通过异常状态401
直接重定向到基础项目的登录页,当不满足需要鉴权的资源请求时候时,定位到文档管理平台相应路径