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
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
}