You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

8.5 KiB

登录回调处理

Handler

package auth

import (
	"fmt"
	"html/template"
	"net/http"

	"backend/internal/logic/auth"
	"backend/internal/svc"
	"backend/internal/types"
	"github.com/zeromicro/go-zero/rest/httpx"
)

const callbackHTMLTemplate = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DevOps 登录回调</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .container {
            background: white;
            border-radius: 16px;
            padding: 40px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            max-width: 500px;
            width: 90%;
        }
        .header { text-align: center; margin-bottom: 30px; }
        .header h1 { color: #333; font-size: 24px; margin-bottom: 8px; }
        .header p { color: #666; font-size: 14px; }
        .status {
            text-align: center;
            padding: 20px;
            border-radius: 12px;
            margin-bottom: 20px;
        }
        .status.success { background: #d4edda; color: #155724; }
        .status.error { background: #f8d7da; color: #721c24; }
        .status-icon { font-size: 48px; margin-bottom: 10px; }
        .user-info {
            background: #f8f9fa;
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 20px;
        }
        .user-info h3 {
            font-size: 14px;
            color: #666;
            margin-bottom: 12px;
            text-transform: uppercase;
        }
        .info-item {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid #e9ecef;
        }
        .info-item:last-child { border-bottom: none; }
        .info-label { color: #666; font-size: 14px; }
        .info-value { color: #333; font-weight: 500; font-size: 14px; }
        .token-section { margin-top: 20px; }
        .token-section h3 { font-size: 14px; color: #666; margin-bottom: 8px; }
        .token-box {
            background: #2d3748;
            color: #68d391;
            padding: 12px;
            border-radius: 8px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            word-break: break-all;
            max-height: 100px;
            overflow-y: auto;
        }
        .actions {
            margin-top: 24px;
            display: flex;
            gap: 12px;
        }
        .btn {
            flex: 1;
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-primary { background: #667eea; color: white; }
        .btn-primary:hover { background: #5a6fd6; }
        .btn-secondary { background: #e2e8f0; color: #4a5568; }
        .btn-secondary:hover { background: #cbd5e0; }
        .copy-hint {
            text-align: center;
            color: #48bb78;
            font-size: 12px;
            margin-top: 8px;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .copy-hint.show { opacity: 1; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>DevOps 平台</h1>
            <p>Casdoor 单点登录</p>
        </div>
        {{if .Success}}
        <div class="status success">
            <div class="status-icon">✅</div>
            <div>登录成功!</div>
        </div>
        <div class="user-info">
            <h3>用户信息</h3>
            <div class="info-item">
                <span class="info-label">用户名</span>
                <span class="info-value">{{.User.Username}}</span>
            </div>
            <div class="info-item">
                <span class="info-label">邮箱</span>
                <span class="info-value">{{.User.Email}}</span>
            </div>
            <div class="info-item">
                <span class="info-label">用户ID</span>
                <span class="info-value">{{.User.Id}}</span>
            </div>
        </div>
        <div class="token-section">
            <h3>访问令牌 (Token)</h3>
            <div class="token-box" id="token">{{.Token}}</div>
            <div class="copy-hint" id="copyHint">已复制到剪贴板!</div>
        </div>
        <div class="actions">
            <button class="btn btn-secondary" onclick="copyToken()">复制 Token</button>
            <button class="btn btn-primary" onclick="testAPI()">测试 API</button>
        </div>
        {{else}}
        <div class="status error">
            <div class="status-icon">❌</div>
            <div>登录失败:{{.Error}}</div>
        </div>
        {{end}}
    </div>
    <script>
        function copyToken() {
            const token = document.getElementById('token').textContent;
            navigator.clipboard.writeText(token).then(() => {
                const hint = document.getElementById('copyHint');
                hint.classList.add('show');
                setTimeout(() => hint.classList.remove('show'), 2000);
            });
        }
        function testAPI() {
            const token = document.getElementById('token').textContent;
            fetch('/api/v1/auth/user', {
                headers: { 'Authorization': 'Bearer ' + token }
            })
            .then(r => r.json())
            .then(data => {
                alert('API 测试成功!\n用户信息:' + JSON.stringify(data, null, 2));
            })
            .catch(err => {
                alert('API 测试失败:' + err);
            });
        }
    </script>
</body>
</html>`

type CallbackPageData struct {
	Success bool
	Token   string
	User    *types.UserInfo
	Error   string
}

func CallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.CallbackReq
		if err := httpx.Parse(r, &req); err != nil {
			renderCallbackPage(w, false, "", nil, err.Error())
			return
		}

		l := auth.NewCallbackLogic(r.Context(), svcCtx)
		resp, err := l.Callback(&req)
		if err != nil {
			renderCallbackPage(w, false, "", nil, err.Error())
			return
		}

		renderCallbackPage(w, true, resp.Token, &resp.User, "")
	}
}

func renderCallbackPage(w http.ResponseWriter, success bool, token string, user *types.UserInfo, errMsg string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	data := CallbackPageData{
		Success: success,
		Token:   token,
		User:    user,
		Error:   errMsg,
	}
	tmpl, _ := template.New("callback").Parse(callbackHTMLTemplate)
	tmpl.Execute(w, data)
}

Logic

package auth

import (
	"backend/internal/svc"
	"backend/internal/types"
	"backend/model"
	"context"
	"time"

	"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
	"gorm.io/gorm"
)

type CallbackLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CallbackLogic {
	return &CallbackLogic{ctx: ctx, svcCtx: svcCtx}
}

func (l *CallbackLogic) Callback(req *types.CallbackReq) (*types.CallbackResp, error) {
	// 换取 Casdoor Token
	authConfig, err := l.svcCtx.Casdoor.ExchangeToken(req.Code)
	if err != nil {
		return nil, err
	}

	casdoorUser := authConfig.Claims.User

	// 查找或创建本地用户
	user, err := l.findOrCreateUser(casdoorUser)
	if err != nil {
		return nil, err
	}

	// 生成本地 JWT
	token, expiresAt, _ := l.svcCtx.JWT.GenerateToken(user.ID, user.Username, user.Email)

	return &types.CallbackResp{
		Token:     token,
		ExpiresAt: expiresAt,
		User: types.UserInfo{
			Id:       user.ID,
			Username: user.Username,
			Email:    user.Email,
			Status:   user.Status,
		},
	}, nil
}

func (l *CallbackLogic) findOrCreateUser(casdoorUser *casdoorsdk.User) (*model.User, error) {
	var user model.User

	// 用用户名查询
	err := l.svcCtx.DB.Where("username = ?", casdoorUser.Name).First(&user).Error
	if err == nil {
		return &user, nil
	}

	if err != gorm.ErrRecordNotFound {
		return nil, err
	}

	// 自动创建新用户
	user = model.User{
		UserType:  "casdoor",
		Username:  casdoorUser.Name,
		Email:     casdoorUser.Email,
		Avatar:    casdoorUser.Avatar,
		Role:      "member",
		Status:    1,
		CreatedAt: time.Now(),
	}
	l.svcCtx.DB.Create(&user)
	return &user, nil
}