使用Casdoor搭建用户身份管理平台

May 30, 2024

计划为博客加上认证(登录)注册能力,最初计划是使用Clerk之类的第三方平台,但是评估一圈之后还是决定采用Casdoor自己搭建。主要的评估因素包括以下几点:

  • 开发成本
  • 支持国内的QQ、Wechat等Provider
  • 价格:Clerk的免费版功能限制挺多的,比如仅允许3个Provider,而现在还没有付费版的诉求。Pro版价格是$25,已经超过我腾讯云机器的价格了。
  • 开源:个人偏好,不要绑定在某一个特定平台。

Casdoor的搭建流程并不复杂,后端使用GO实现,理论上资源消耗应该也不大,遂决定在我的vps上安装。

介绍

Casdoor是一个开源的身份和访问管理(IAM)/单点登录(SSO)平台。其主要特性包括:

  • 前后端分离的架构,前端使用react,后端使用 Go 语言开发,Casdoor 支持高并发,提供基于Web的用户管理 UI,并支持中、英等多种语言
  • Casdoor 支持 Github, Google, QQ, WeChat, Facebook, Gitee 等第三方应用程序登录,并支持使用插件扩展第三方登录
  • 使用 Casbin 基于授权管理,Casdoor 支持 ACL, RBAC, ABAC, RESTful 等访问控制模型
  • 个性化的注册、登录和忘记密码页面。支持手机验证码、邮件验证码、找回密码等功能
  • 使用阿里云、腾讯云、七牛云提供的图片CDN云存储
  • Casdoor 支持使用 db 同步方法与现有系统的集成,用户可以顺利过渡到Casdoor
  • Casdoor 支持的主流数据库: MySQL, SQL Server, PostgreSQL 等, 并支持扩展插件的新数据库

安装

官方提供了多种安装方式,包括docker安装、本地服务端安装等,参考官方文档。

我采用的是本地server安装的方式,大致流程:

  1. git clone 后编译前端web。
  2. 编译后端go sever。此时已经已经将前后端打包成一个可执行文件了。
  3. 创建mysql database。
  4. 修改 conf/app.conf 配置文件中的DB配置
  5. 使用pm2启动服务。
  6. 配置Nginx、分配单独的子域名以及SSL证书。

注意点

  • 不支持path:所以需要有独立域名。我使用的自己域名下的独立子域名。后续的SMTP也使用此域名。
  • 编译前端react时内存消耗较大,我的4G的vps竟然有点吃力。看官方文档应该需要2G左右,禁用sourcemap后才编译通过。

配置

使用前登录管理台,做了一些简单的配置。

基础配置

  1. 安全措施:修改管理员默认秘钥,禁止默认app注册。
  2. 新增orgnization、application,供个人博客使用。
  3. 注册了github provider。
  4. application中去掉了一些不必要的信息,比如手机号,简化注册流程。
  5. 登录页样式做了一些简单的调整。

SMTP

为了提供注册能力,需要提供一个可用的EMail Provier,用于验证邮箱以及找回密码登。对比一下官方文章中的几种Email Provider。

sendgrid

  • 免费版:免费额度 100次/天。
  • Essentials版:$20/月。

Azure

  • 按使用量

brevo

  • 免费版:免费额度300次/天
  • Starter:5000/月
  • 注册有地区限制

尝试一圈后放弃,注册流程太麻烦了,国内手机没法使用。最终看了腾讯云的SMTP服务并不贵,于是按照文档安装完毕。

💡
增加email provider时遇到一个bug:需要先保存然后才能测试STMP connnection的可用性。否则一直会有个奇怪的无法连接错误。原因是服务端没有使用页面上填写stmp server信息进行验证,已经反馈给官方。

其他

增加了部分Provider,比如GIT,用于支持第三方账号的直接登录。后续再慢慢增加其他类型。

使用

按照官方的列子,Next项目使用相关的改动点,包括以下部分:

  1. 增加认证拦截中间件:未认证用户拦截跳转到登录页page。
[javascript]
const protectedRoutes = ["/profile"];

export default function middleware(req) {
  const casdoorUserCookie = req.cookies.get("casdoorUser");
  const isAuthenticated = casdoorUserCookie ? true : false;

  if (!isAuthenticated && protectedRoutes.includes(req.nextUrl.pathname)) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
}
  1. 增加登录page:这里的登录页面不需要我们实现,直接跳转到casdoor的页面即可。可以理解为casdoor帮助我们完成了认证(登录)、注册的能力,然后将认证成功的用户信息(code)通过指定的redirec参数t地址返回给到我们。
[javascript]
const CasdoorConfig = {
  serverUrl: process.env.NEXT_PUBLIC_CASDOOR_HOST || "https://door.casbin.com",
  clientId: process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "default",
  organizationName: process.env.NEXT_PUBLIC_CASDOOR_ORG || "casdoor",
  appName: process.env.NEXT_PUBLIC_CASDOOR_APP || "default",
  redirectPath: "/login/result",
};

const Login = () => {
  useEffect(() => {
    const CasdoorSDK = new Sdk(CasdoorConfig);
    CasdoorSDK.signin_redirect();
  }, []);

  return <></>;
};

export default Login;

上面的client、orgnization、app等需要在casdoor上面自行提前分配好。

  1. 用户在casdoor页面上完成注册、认证之后,casdoor会将认证 code 通过redirect的方式给到应用(即上面的redirectPath)。应用需要将code换取为 access_token ,并通过access_token获取用户详细信息,存放到cookie中。
[javascript]
export const AuthCallback = () => {
  const router = useRouter();

  useEffect(() => {
    const CasdoorSDK = new Sdk(CasdoorConfig);

    CasdoorSDK.exchangeForAccessToken()
      .then((res) => {
        if (res && res.access_token) {
          return CasdoorSDK.getUserInfo(res.access_token);
        } else {
          throw new Error(res.error_description);
        }
      })
      .then((res) => {
        const casdoorUserInfo = res;
        Cookies.set("casdoorUser", JSON.stringify(casdoorUserInfo));
        router.push("/profile");
      })
      .catch((error) => {
        router.push("/");
      });
  }, []);

  return <div>signing...</div>;
};

获取到的票据包括以下字段

[json]
{
    "access_token": "xxxx.xxx.xxx",
    "id_token": "",
    "refresh_token": "xxx",
    "token_type": "Bearer",
    "expires_in": 259200,
    "scope": "profile"
}

上面只是演示了OIDC基础使用流程,使用了session cookie来存储,如果需要持久化存储,还有一些关于票据安全的优化点需要考虑。

票据安全

cookie有效期

上面设置cookie使用的是session cookie,即关闭浏览器cookie就会丢失,下次访问时需要重新认证,但是这不能阻挡用户故意存储票据用于下次访问。理论上accesstoken是有个过期时间的。在casdoor服务端上配置,这里需要有一定的机制来保证用户的票据在超过过期时间expires_in之后无效。

cookie内容合法性

上面的middleware仅仅校验了cookie中是否存在user信息,并没有校验用户的合法性。页面是可以篡改cookie数据的,比如将用户id、name替换,如果后端直接使用cookie中的user信息,就会有安全风险。

更加合适的做法是将accesstoken当做票据,并在middleware中校验accesstoken的有效性。使其承担起接入层的作用。accesstoken可以访问cosdoor去校验,或者在业务服务端存储,基本不可能伪造(但是也有被共享的可能)。

参考Casnode示例。使用了session来存储ParseJwtToken()的claim结果,里面包换了accesstoken以及用户信息。

[go]
func (c *ApiController) Signin() {
	code := c.Input().Get("code")
	state := c.Input().Get("state")

	token, err := auth.GetOAuthToken(code, state)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	claims, err := auth.ParseJwtToken(token.AccessToken)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	affected, err := object.UpdateMemberOnlineStatus(&claims.User, true, util.GetCurrentTime())
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	claims.AccessToken = token.AccessToken
	c.SetSessionClaims(claims)

	c.ResponseOk(claims, affected)
}

票据更新

理论上accesstoken的过期时间不应该太长,比如2小时。然后通过票据刷新接口定期刷新accesstoken,避免使用过程中的泄露。

吐槽

在自己的接入SDk的过程中,发现一些小的问题。

  1. SDK参数各异:比如js版前后端版本的SDK,url、org都能有两种写法
[javascript]
// in ./backend/server.js
const authCfg = {
  endpoint: 'https://door.casdoor.com',
  clientId: '014ae4bd048734ca2dea',
  clientSecret: 'f26a4115725867b7bb7b668c81e1f8f7fae1544d',
  certificate: cert,
  orgName: 'casbin',
  appName: 'app-casnode',
}
[javascript]
// in ./src/Setting.js
const config = {
  serverUrl: "https://door.casdoor.com",
  clientId: "014ae4bd048734ca2dea",
  organizationName: "casbin",
  appName: "app-casnode",
  redirectPath: "/callback",
};
  1. SDK鉴权方式差异,在官方文档中,前后端鉴权所需的参数还挺大的。在我看来使用的目的相同,都是取用户资料,为何后端还需要cert、secret key等,而前端sdk啥也不用?cert、secret这种是否有必要并存。
  2. SDK能力不完善。个人项目是使用nextjs,安全考虑,期望是将accsstoken获取、解析放在后端,避免jwt透传。提供的next SDK仅在client component下可用,server端根本没办法使用。转而使用node版本的SDK,发现在next下无法使用(依赖的 jsonwebtoken 在next 环境不兼容)。

总结

以上就是casdoor的用户认证基础流程,基础的用户认证管理接入还是挺简单的。casdoor还提供了不少其他能力,包括商品管理&教育、用户群组管理。

 

 

See all postsSee all posts