# casdoor 用户认证使用

## 安装

- 编译
  1. [下载源码](https://github.com/casdoor/casdoor)
  2. 编译后端服务：`go build .`
  3. 编译前端：`yarn build`
- 配置

编写Dockerfile文件：

```sh
FROM alpine

RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.15/main > /etc/apk/repositories \
  && cat /etc/apk/repositories \
# 设置时区为上海
  && apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
  && echo "Asia/Shanghai" > /etc/timezone \
  && apk del tzdata \
# 解决apline 运行编译后的执行文件 not found错误
# 由于alpine镜像使用的是musl libc而不是gnu libc，/lib64/ 是不存在的。但他们是兼容的，可以创建个软连接
  && mkdir /lib64 \
  && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

WORKDIR /

ADD web web
ADD casdoor casdoor

EXPOSE 8000

ENTRYPOINT ["./casdoor"]
```

编写镜像构建脚本：

```sh
#!/bin/bash

# 命名镜像
repository_image="registry.cn-chengdu.aliyuncs.com/infish/pack-casdoor-auth:v1.0.0"

# 删除本地已存在的镜像
docker rmi $repository_image

# 创建本地镜像
docker build -t $repository_image .

# push到镜像仓库，需要登陆对应docker仓库账号
docker push $repository_image
```

编写docker-compose.yaml文件：

```sh

version: '3.8'

# 网络
networks:
  default:
    name: default-network
    external: true

services:
  auth-casdoor:
    image: "registry.cn-chengdu.aliyuncs.com/infish/pack-casdoor-auth:v1.0.0"
    restart: always
    ports:
      - 36002:8000
    volumes:
      - ./conf:/conf
    depends_on:
        - auth-mysql

  auth-mysql:
    restart: always
    # 5.7不支持中文
    image: mysql:8
    volumes:
      - /data/auth/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: auth2023
      MYSQL_DATABASE: casdoor
    ports:
      - 33306:3306
```

conf/app.conf文件：

```sh
ppname = casdoor
httpport = 8000
runmode = dev
copyrequestbody = true
driverName = mysql
dataSourceName = root:auth2023@tcp(auth-mysql:3306)/
dbName = casdoor
# tableNamePrefix =
# showSql = false
# redisEndpoint =
# defaultStorageProvider =
# isCloudIntranet = false
# authState = "casdoor"
# socks5Proxy = "127.0.0.1:10808"
# verificationCodeTimeout = 10
# initScore = 2000
# logPostOnly = true
# origin =
# staticBaseUrl = "https://auth3dqueen.oss-cn-beijing.aliyuncs.com/static"
# isDemoMode = false
# batchSize = 100
# ldapServerPort = 389
# languages = en,zh,es,fr,de,id,ja,ko,ru,vi
# quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
```

启动服务：`docker compose up -d`

配置nginx：

```conf
server {
    listen 80;
    listen [::]:80;
    server_name auth.3dqueen.cloud;
    rewrite ^(.*) https://$server_name$1 permanent;
 }

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name auth.3dqueen.cloud;
    #root /var/www/auth;

    client_max_body_size 5M;
    ssl_certificate "cert/9743199_auth.3dqueen.cloud.pem";
    ssl_certificate_key "cert/9743199_auth.3dqueen.cloud.key";
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;


    location / {
        proxy_set_header    Host            $http_host;
        proxy_set_header    X-Real-IP       $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect      off;
        proxy_pass http://127.0.0.1:36002;
    }

}

```

## 配置应用

访问nginx配置的域名，登录后配置组织和应用：`账号：admin 密码：123`

组织配置：![组织配置](images/组织.png)
应用配置：![应用配置](images/应用.png)

## 认证

配置证书：![证书配置](images/证书.png)

下载证书、配置到后端：

app.yaml:

```yaml
...
Auth:
  endpoint: https://auth.3dqueen.cloud
  clientId:	9f22d6616ae14fe59dc4
  clientSecret: b0fae13ca7e7eb1e4e6dc8fa50902eb7b6035049
  certificate: token_jwt_key.pem
  organizationName:	org_3dqueen_cloud
  applicationName: app_boxcost
```

Dockerfile文件中添加：

```Dockerfile
...
# 下载的证书
ADD token_jwt_key.pem token_jwt_key.pem
...
```

下载使用[casdoor-go-sdk](https://github.com/casdoor/casdoor-go-sdk)

配置回调：`api/calback.go`

```go
// https://auth.3dqueen.cloud/login/oauth/authorize?client_id=9f22d6616ae14fe59dc4&redirect_uri=https://www.3dqueen.cloud/box/v1/boxcost/callback&response_type=code&scope=openid&state=STATE
// 需要在第三方提供商配置回调 https://auth.3dqueen.cloud/callback
func callback(c *gin.Context, apictx *ApiSession) (interface{}, error) {
	authConf := apictx.Svc.Conf.Auth
	pemByte, err := os.ReadFile(authConf.Certificate)
	if err != nil {
		return nil, err
	}
	casdoorsdk.InitConfig(authConf.Endpoint, authConf.ClientId, authConf.ClientSecret, string(pemByte), authConf.OrganizationName, authConf.ApplicationName)
	token, err := casdoorsdk.GetOAuthToken(c.Query("code"), c.Query("state"))
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(token.AccessToken)
	claims, err := casdoorsdk.ParseJwtToken(token.AccessToken)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(claims)
	c.Redirect(http.StatusFound, "https://auth.3dqueen.cloud")

	// claims.AccessToken = token.AccessToken
	return nil, nil
}

```

password方式获取token：`api/auth.go`

```go
func AuthToken(c *gin.Context, apictx *ApiSession) (interface{}, error) {

	authConf := apictx.Svc.Conf.Auth
	// pemByte, err := os.ReadFile(authConf.Certificate)
	// if err != nil {
	// 	return nil, err
	// }
	// casdoorsdk.InitConfig(authConf.Endpoint, authConf.ClientId, authConf.ClientSecret, string(pemByte), authConf.OrganizationName, authConf.ApplicationName)

	data := &AuthTOkenReq{
		GrantType:    "password",
		ClientId:     authConf.ClientId,
		ClientSecret: authConf.ClientSecret,
		UserName:     "sunsheng",
		Password:     "ssxhyw2515",
	}
	dataByte, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}
	fmt.Println(dataByte)

	// 发送POST请求并将数据作为JSON提交
	req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", authConf.Endpoint, "api/login/oauth/access_token"), bytes.NewBuffer(dataByte))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var buf bytes.Buffer
	_, err = buf.ReadFrom(resp.Body)
	if err != nil {
		panic(err)
	}
	buf.WriteTo(c.Writer)
	// responseStr := buf.String()
	// 打印响应内容
	// fmt.Println(responseStr)

	return nil, nil
}
```

## 权限控制

配置权限模型：![权限模型](images/模型.png)
配置权限：![权限模型](images/权限.png)

jwt中间件：`middleware/jwt.go`

```go
package middleware

import (
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
	"github.com/gin-gonic/gin"
)

// JWTAuthMiddleware 基于JWT的认证中间件--验证用户是否登录
func JWTAuthMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		authHeader := c.Request.Header.Get("authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 2003,
				"msg":  "请求头中auth为空",
			})
			c.Abort()
			return
		}
		// 按空格分割
		parts := strings.Split(authHeader, ".")
		if len(parts) != 3 {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 2004,
				"msg":  "请求头中auth格式有误",
			})
			c.Abort()
			return
		}
		mc, err := casdoorsdk.ParseJwtToken(authHeader)
		fmt.Println(mc.VerifyExpiresAt(time.Now(), false))
		if err != nil {
			fmt.Println(err)
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 2005,
				"msg":  "无效的Token",
			})
			c.Abort()
			return
		}

		// m := mc.(jwt.MapClaims)
		// 将当前请求的username信息保存到请求的上下文c上
		c.Set("uid", mc.User.Id)
		c.Set("username", mc.User.Name)
		c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
	}
}

```

权限控制中间件：`middleware/pc.go`

```go
package middleware

import (
	"boxcost/conf"
	"fmt"
	"net/http"
	"strings"

	"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
	"github.com/gin-gonic/gin"
)

func PermissionControlMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {
		// 验证该用户是否可以访问该资源

		permission := strings.Replace(c.Request.URL.String(), "/", "_", -1)

		id := fmt.Sprintf("%s/%s%s", conf.AppConfig.Auth.OrganizationName, strings.ToLower(c.Request.Method), permission)
		fmt.Println(id)
		ok, err := casdoorsdk.Enforce(&casdoorsdk.PermissionRule{
			Ptype: "p",
			V0:    fmt.Sprintf("%s/%s", conf.AppConfig.Auth.OrganizationName, c.GetString("username")), // 请求实体
			V1:    c.Request.URL.String(),                                                              // 请求资源主体 // /book/*
			V2:    strings.ToLower(c.Request.Method),                                                   // 请求方法
			Id:    id,
		})
		// roles
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": -1,
				"msg":  err.Error(),
			})
			c.Abort()
			return
		}
		if !ok {
			c.JSON(http.StatusForbidden, gin.H{
				"code": -1,
				"msg":  "没有权限访问",
			})
			c.Abort()
			return
		}
		c.Next()

	}

}

```

使用中间件：`api/api.go`

```go
...
// GETJWT http Get 请求
func (g GinRouter) GETJWT(path string, httpHandler JWTHander) {
	g.group.GET(path, middleware.JWTAuthMiddleware(), middleware.PermissionControlMiddleware(), ResultJWTWrapper(httpHandler, g.svc))
}

// POSTJWT http POST 请求
func (g GinRouter) POSTJWT(path string, httpHandler JWTHander) {
	g.group.POST(path, middleware.JWTAuthMiddleware(), middleware.PermissionControlMiddleware(), ResultJWTWrapper(httpHandler, g.svc))
}
...
```

测试：`api/user.go`

```go
package api

import (
	"boxcost/conf"
	"fmt"
	"time"

	"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
	"github.com/gin-gonic/gin"
)

func User(r *GinRouter) {
	r.POSTJWT("/user/create", CreateUser)
}

func CreateUser(c *gin.Context, apictx *ApiSession) (interface{}, error) {
	var user casdoorsdk.User
	err := c.ShouldBindJSON(&user)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	user.SignupApplication = conf.AppConfig.Auth.ApplicationName
	user.CreatedTime = time.Now().Format("2006-01-02T15:04:05-07:00")
	user.Type = "normal-user"

	return casdoorsdk.AddUser(&user)
}
```

测试api：![测试api](images/测试api.png)

## 其他

配置短信：![短信配置](images/短信配置.png)
第三方登录：![第三方登录配置](images/第三方登录.png)
对象存储：![对象存储配置](images/对象存储.png)