🎉 完成基本的演示和样例
This commit is contained in:
86
internal/serve/handler/approve.go
Normal file
86
internal/serve/handler/approve.go
Normal file
@ -0,0 +1,86 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
"uauth/internal/store/cache"
|
||||
"uauth/internal/store/db"
|
||||
"uauth/model"
|
||||
)
|
||||
|
||||
//go:embed serve_approve.html
|
||||
var pageApprove string
|
||||
|
||||
func Approve(c *nf.Ctx) error {
|
||||
// 获取表单数据
|
||||
type Req struct {
|
||||
ClientId string `form:"client_id"`
|
||||
RedirectURI string `form:"redirect_uri"`
|
||||
Scope string `form:"scope"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
uri *url.URL
|
||||
client = new(model.Client)
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err)
|
||||
}
|
||||
|
||||
if uri, err = url.Parse(req.RedirectURI); err != nil {
|
||||
log.Warn("[S] parse redirect uri = %s, err = %s", req.RedirectURI, err.Error())
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid redirect uri")
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Where("client_id", req.ClientId).Take(client).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid client_id")
|
||||
}
|
||||
|
||||
log.Error("[S] get client by id fail, client_id = %s, err = %s", req.ClientId, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
db.Default.Session().Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&model.AuthorizationRecord{
|
||||
UserId: op.Id,
|
||||
ClientId: client.Id,
|
||||
})
|
||||
|
||||
authorizationCode := uuid.New().String()[:8]
|
||||
if err = cache.Client.SetEx(c.Context(), cache.Prefix+"auth_code:"+authorizationCode, op.Id, 10*time.Minute); err != nil {
|
||||
return resp.Resp500(c, err)
|
||||
}
|
||||
|
||||
qs := uri.Query()
|
||||
qs.Add("code", authorizationCode)
|
||||
qs.Add("client_id", req.ClientId)
|
||||
qs.Add("scope", req.Scope)
|
||||
qs.Add("state", req.State)
|
||||
|
||||
uri.ForceQuery = true
|
||||
value := uri.String() + qs.Encode()
|
||||
|
||||
return c.RenderHTML("approve", pageApprove, map[string]interface{}{
|
||||
"redirect_uri": value,
|
||||
})
|
||||
}
|
121
internal/serve/handler/authorize.go
Normal file
121
internal/serve/handler/authorize.go
Normal file
@ -0,0 +1,121 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
"uauth/internal/store/cache"
|
||||
"uauth/internal/store/db"
|
||||
"uauth/model"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed serve_authorize.html
|
||||
pageAuthorize string
|
||||
)
|
||||
|
||||
func Authorize(c *nf.Ctx) error {
|
||||
|
||||
type Req struct {
|
||||
ClientId string `query:"client_id"`
|
||||
ResponseType string `query:"response_type"`
|
||||
RedirectURI string `query:"redirect_uri"`
|
||||
Scope string `query:"scope"`
|
||||
State string `query:"state"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
err error
|
||||
client = &model.Client{}
|
||||
authRecord = &model.AuthorizationRecord{}
|
||||
uri *url.URL
|
||||
)
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
log.Error("[S] query parser err = %s", err.Error())
|
||||
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
||||
}
|
||||
|
||||
if req.ResponseType != "code" {
|
||||
log.Warn("[S] response type = %s", req.ResponseType)
|
||||
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
||||
}
|
||||
|
||||
// 如果未登录,则跳转到登录界面
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
log.Info("[S] op not logined, redirect to login page")
|
||||
return c.Redirect("/oauth/v2/login?"+c.Request.URL.Query().Encode(), http.StatusFound)
|
||||
}
|
||||
|
||||
log.Info("[S] Authorize: username = %s, client_id = %s", op.Username, req.ClientId)
|
||||
|
||||
if err = db.Default.Session().Where("client_id", req.ClientId).Take(client).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid client_id")
|
||||
}
|
||||
|
||||
log.Error("[Authorize]: db take clients err = %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Model(&model.AuthorizationRecord{}).
|
||||
Where("user_id", op.Id).
|
||||
Where("client_id", client.Id).
|
||||
Take(authRecord).
|
||||
Error; err != nil {
|
||||
// 用户第一次对该 client 进行授权
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
return c.RenderHTML("authorize", pageAuthorize, map[string]any{
|
||||
"user": map[string]any{
|
||||
"username": op.Username,
|
||||
"avatar": "https://picsum.photos/200",
|
||||
},
|
||||
"client_id": req.ClientId,
|
||||
"redirect_uri": req.RedirectURI,
|
||||
"scope": req.Scope,
|
||||
"state": req.State,
|
||||
})
|
||||
}
|
||||
|
||||
log.Error("[Authorize]: db take authorization_records err = %s", err.Error())
|
||||
return resp.Resp500(c, err)
|
||||
}
|
||||
|
||||
// 当用户已经授权过时
|
||||
// 生成授权码并缓存授权码
|
||||
log.Debug("[Authorize]: username = %s already approved %s", op.Username, client.Name)
|
||||
|
||||
authorizationCode := uuid.New().String()[:8]
|
||||
if err = cache.Client.SetEx(c.Context(), cache.Prefix+"auth_code:"+authorizationCode, op.Id, 10*time.Minute); err != nil {
|
||||
return resp.Resp500(c, err)
|
||||
}
|
||||
|
||||
if uri, err = url.Parse(req.RedirectURI); err != nil {
|
||||
log.Warn("[S] parse redirect uri = %s, err = %s", req.RedirectURI, err.Error())
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid redirect uri")
|
||||
}
|
||||
|
||||
qs := uri.Query()
|
||||
qs.Add("code", authorizationCode)
|
||||
qs.Add("client_id", req.ClientId)
|
||||
qs.Add("scope", req.Scope)
|
||||
qs.Add("state", req.State)
|
||||
|
||||
uri.ForceQuery = true
|
||||
value := uri.String() + qs.Encode()
|
||||
|
||||
return c.RenderHTML("approve", pageApprove, map[string]interface{}{
|
||||
"redirect_uri": value,
|
||||
})
|
||||
}
|
137
internal/serve/handler/login.go
Normal file
137
internal/serve/handler/login.go
Normal file
@ -0,0 +1,137 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"time"
|
||||
"uauth/internal/store/cache"
|
||||
"uauth/internal/store/db"
|
||||
"uauth/internal/tool"
|
||||
"uauth/model"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed serve_login.html
|
||||
pageLogin string
|
||||
)
|
||||
|
||||
func LoginPage(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
ClientId string `query:"client_id" json:"client_id"`
|
||||
Scope string `query:"scope" json:"scope"`
|
||||
RedirectURI string `query:"redirect_uri" json:"redirect_uri"`
|
||||
State string `query:"state" json:"state"`
|
||||
ResponseType string `query:"response_type" json:"response_type"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
client = new(model.Client)
|
||||
)
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.ClientId == "" || req.RedirectURI == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Model(&model.Client{}).
|
||||
Where("client_id = ?", req.ClientId).
|
||||
Take(client).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusForbidden).SendString("Client Not Registry")
|
||||
}
|
||||
|
||||
log.Error("[S] model take client id = %s, err = %s", req.ClientId, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
return c.RenderHTML("login", pageLogin, map[string]interface{}{
|
||||
"client_id": req.ClientId,
|
||||
"redirect_uri": req.RedirectURI,
|
||||
"scope": req.Scope,
|
||||
"state": req.State,
|
||||
"response_type": req.ResponseType,
|
||||
"client_name": client.Name,
|
||||
"client_icon": client.Icon,
|
||||
})
|
||||
}
|
||||
|
||||
//go:embed serve_login_success.html
|
||||
var pageLoginSuccess string
|
||||
|
||||
func LoginAction(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
ClientId string `form:"client_id"`
|
||||
RedirectURI string `form:"redirect_uri"`
|
||||
Scope string `form:"scope"`
|
||||
State string `form:"state"`
|
||||
ResponseType string `form:"response_type"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
op = new(model.User)
|
||||
token string
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
log.Warn("[S] LoginAction: body parser err = %s", err.Error())
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: username, password is required")
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Model(&model.User{}).
|
||||
Where("username = ?", req.Username).
|
||||
Take(op).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Warn("[S] LoginAction: username = %s not found", req.Username)
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
||||
}
|
||||
|
||||
log.Error("[S] LoginAction: model take username = %s, err = %s", req.Username, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
// todo: 验证用户登录是否成功,等等
|
||||
if !tool.ComparePassword(req.Password, op.Password) {
|
||||
log.Warn("[S] LoginAction: model take username = %s, password is invalid", req.Username)
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
||||
}
|
||||
|
||||
if token, err = op.JwtEncode(); err != nil {
|
||||
log.Error("[S] LoginAction: jwtEncode err = %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
key := cache.Prefix + "token:" + token
|
||||
if err = cache.Client.SetEx(c.Context(), key, op, 24*time.Hour); err != nil {
|
||||
log.Error("[S] LoginAction: cache SetEx err = %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
c.Writer.Header().Add("Set-Cookie", "access_token="+token)
|
||||
|
||||
return c.RenderHTML("login_success", pageLoginSuccess, map[string]interface{}{
|
||||
"client_id": req.ClientId,
|
||||
"redirect_uri": req.RedirectURI,
|
||||
"scope": req.Scope,
|
||||
"state": req.State,
|
||||
"response_type": req.ResponseType,
|
||||
})
|
||||
}
|
111
internal/serve/handler/registry.go
Normal file
111
internal/serve/handler/registry.go
Normal file
@ -0,0 +1,111 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"gorm.io/gorm"
|
||||
"uauth/internal/store/db"
|
||||
"uauth/internal/tool"
|
||||
"uauth/model"
|
||||
)
|
||||
|
||||
func ClientRegistry(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Icon string `json:"icon"` // url
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
Secret := tool.RandomString(32)
|
||||
|
||||
platform := &model.Client{
|
||||
ClientId: req.ClientId,
|
||||
Icon: req.Icon,
|
||||
Name: req.Name,
|
||||
ClientSecret: Secret,
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Create(platform).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return resp.Resp400(c, err, "当前平台已经存在")
|
||||
}
|
||||
|
||||
return resp.Resp500(c, err)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, platform)
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed serve_registry.html
|
||||
registryLogin string
|
||||
)
|
||||
|
||||
func UserRegistryPage(c *nf.Ctx) error {
|
||||
return c.HTML(registryLogin)
|
||||
}
|
||||
|
||||
func UserRegistryAction(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `form:"username"`
|
||||
Nickname string `form:"nickname"`
|
||||
Password string `form:"password"`
|
||||
ConfirmPassword string `form:"confirm_password"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, req, err.Error())
|
||||
}
|
||||
|
||||
op := &model.User{
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
Password: tool.NewPassword(req.Password),
|
||||
}
|
||||
|
||||
if err = db.Default.Session().Create(op).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return resp.Resp400(c, err, "用户名已存在")
|
||||
}
|
||||
|
||||
return resp.Resp500(c, err)
|
||||
}
|
||||
|
||||
return c.HTML(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<title>注册成功</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>注册成功</h1>
|
||||
<h3>快去试试吧</h3>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
33
internal/serve/handler/serve_approve.html
Normal file
33
internal/serve/handler/serve_approve.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>授权成功</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span aria-busy="true">授权成功, 正在跳转回原网页...</span>
|
||||
<div style="display: none">
|
||||
<input type="hidden" id="redirect_uri" value="{{ .redirect_uri }}"/>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => {
|
||||
console.log('[D] after 1s console')
|
||||
let redirect_uri = document.getElementById('redirect_uri').value
|
||||
window.location.href = redirect_uri
|
||||
}, 1000)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
48
internal/serve/handler/serve_authorize.html
Normal file
48
internal/serve/handler/serve_authorize.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<title>Server Login</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h3>授权登录到 {{ .client_name }} 平台</h3>
|
||||
<div class="userinfo">
|
||||
<article style="display: flex; align-items: center;">
|
||||
<div style="height:50px; width:50px;">
|
||||
<img src="{{ .user.avatar }}"/>
|
||||
</div>
|
||||
<div style="margin-left:20px; ">
|
||||
{{ .user.username }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<form action="/oauth/v2/approve" method="POST">
|
||||
<fieldset>
|
||||
<input type="hidden" name="client_id" value="{{ .client_id }}"/>
|
||||
<input type="hidden" name="redirect_uri" value="{{ .redirect_uri }}"/>
|
||||
<input type="hidden" name="scope" value="{{ .scope }}"/>
|
||||
<input type="hidden" name="state" value="{{ .state }}"/>
|
||||
</fieldset>
|
||||
|
||||
<div style="display: flex;">
|
||||
<button type="button" class="contrast" style="flex:1; margin-right: 10px">取消</button>
|
||||
<button type="submit" style="flex: 1;">授权</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
89
internal/serve/handler/serve_login.html
Normal file
89
internal/serve/handler/serve_login.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<title>Server Login</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
div.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 33%;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
div.row:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
div.row:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h3>欢迎来到 UAuth</h3>
|
||||
<article style="display: flex; align-items: center;">
|
||||
<div class="row">
|
||||
<div style="height:50px; width:50px;">
|
||||
<img src="https://picsum.photos/seed/drealism/200"/>
|
||||
</div>
|
||||
<div style="margin-left:10px; ">UAuth</div>
|
||||
</div>
|
||||
<div style="transform: rotate(90deg);" class="row">
|
||||
<svg t="1730168415342" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1981" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40"><path d="M428.3 66.4c-12-5-25.7-2.2-34.9 6.9l-320 319.6c-12.5 12.5-12.5 32.7 0 45.3 12.5 12.5 32.7 12.5 45.3 0l265.4-265V928c0 17.7 14.3 32 32 32s32-14.3 32-32V96c-0.1-12.9-7.9-24.6-19.8-29.6zM950.6 585.8c-12.5-12.5-32.8-12.5-45.3 0L640 850.8V96c0-17.7-14.3-32-32-32s-32 14.3-32 32v832c0 12.9 7.8 24.6 19.7 29.6 4 1.6 8.1 2.4 12.2 2.4 8.3 0 16.5-3.2 22.6-9.4l320-319.6c12.6-12.4 12.6-32.7 0.1-45.2z" p-id="1982"></path></svg>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="height:50px; width:50px;">
|
||||
<img src="{{ .client_icon }}"/>
|
||||
</div>
|
||||
<div style="margin-left:10px; ">
|
||||
{{ .client_name }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<form action="/oauth/v2/login" method="POST">
|
||||
<fieldset>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
name="username"
|
||||
placeholder="username"
|
||||
autocomplete="given-name"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="password"
|
||||
autocomplete="password"
|
||||
/>
|
||||
</label>
|
||||
<input type="hidden" name="client_id" value="{{ .client_id }}"/>
|
||||
<input type="hidden" name="redirect_uri" value="{{ .redirect_uri }}"/>
|
||||
<input type="hidden" name="scope" value="{{ .scope }}"/>
|
||||
<input type="hidden" name="state" value="{{ .state }}" />
|
||||
<input type="hidden" name="response_type" value="{{ .response_type }}" />
|
||||
</fieldset>
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="登录"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
41
internal/serve/handler/serve_login_success.html
Normal file
41
internal/serve/handler/serve_login_success.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录成功</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span aria-busy="true">登录成功, 正在跳转...</span>
|
||||
<div style="display: none">
|
||||
<input type="hidden" id="client_id" value="{{ .client_id }}"/>
|
||||
<input type="hidden" id="scope" value="{{ .scope }}"/>
|
||||
<input type="hidden" id="state" value="{{ .state }}"/>
|
||||
<input type="hidden" id="redirect_uri" value="{{ .redirect_uri }}"/>
|
||||
<input type="hidden" id="response_type" value="{{ .response_type }}"/>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => {
|
||||
console.log('[D] after 1s console')
|
||||
let client_id = document.querySelector('#client_id').value;
|
||||
let scope = document.querySelector('#scope').value;
|
||||
let state = document.querySelector('#state').value;
|
||||
let redirect_uri = document.querySelector('#redirect_uri').value;
|
||||
let response_type = document.querySelector('#response_type').value;
|
||||
window.location.href = `/oauth/v2/authorize?client_id=${client_id}&scope=${scope}&redirect_uri=${redirect_uri}&state=${state}&response_type=${response_type}`;
|
||||
}, 1000)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
70
internal/serve/handler/serve_registry.html
Normal file
70
internal/serve/handler/serve_registry.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
||||
>
|
||||
<title>Server Login</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h3>欢迎注册 UAuth</h3>
|
||||
<form action="/api/oauth/v2/registry/user" method="POST" id="form">
|
||||
<fieldset>
|
||||
<label>
|
||||
用户名
|
||||
<input id="username" name="username" autocomplete="given-name"/>
|
||||
</label>
|
||||
<label>
|
||||
昵称
|
||||
<input id="nickname" name="nickname" autocomplete="given-name"/>
|
||||
</label>
|
||||
<label>
|
||||
密码
|
||||
<input id="password" type="password" name="password" autocomplete="password"/>
|
||||
</label>
|
||||
<label>
|
||||
重复密码
|
||||
<input id="confirm_password" type="password" name="confirm_password" autocomplete="password"/>
|
||||
</label>
|
||||
<button type="button" style="flex: 1;width: 100%;" onclick="registry()">注册</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
function registry() {
|
||||
let user = {
|
||||
username: document.querySelector("#username").value,
|
||||
nickname: document.querySelector("#nickname").value,
|
||||
password: document.querySelector("#password").value,
|
||||
confirm_password: document.querySelector("#confirm_password").value,
|
||||
}
|
||||
|
||||
console.log('[D] user = ', user)
|
||||
|
||||
if (!user.username || !user.password || !user.nickname) {
|
||||
window.alert("参数均不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if (user.password !== user.confirm_password) {
|
||||
window.alert("两次密码不一致")
|
||||
return
|
||||
}
|
||||
|
||||
document.querySelector("#form").submit()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
109
internal/serve/handler/token.go
Normal file
109
internal/serve/handler/token.go
Normal file
@ -0,0 +1,109 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
"uauth/internal/store/cache"
|
||||
"uauth/internal/store/db"
|
||||
"uauth/internal/tool"
|
||||
"uauth/model"
|
||||
)
|
||||
|
||||
func HandleToken(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Code string `form:"code"`
|
||||
GrantType string `form:"grant_type"`
|
||||
RedirectURI string `form:"redirect_uri"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
opId uint64
|
||||
op = new(model.User)
|
||||
token string
|
||||
basic string
|
||||
bs []byte
|
||||
strs []string
|
||||
client = new(model.Client)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid form")
|
||||
}
|
||||
|
||||
// client_secret
|
||||
if basic = c.Get("Authorization"); basic == "" {
|
||||
return c.Status(http.StatusUnauthorized).SendString("Authorization header missing")
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(basic, "Basic "):
|
||||
basic = strings.TrimPrefix(basic, "Basic ")
|
||||
default:
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: authorization scheme not supported")
|
||||
}
|
||||
|
||||
if bs, err = base64.StdEncoding.DecodeString(basic); err != nil {
|
||||
log.Warn("[Token] base64 decode failed, raw = %s, err = %s", basic, err.Error())
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid basic authorization")
|
||||
}
|
||||
|
||||
if strs = strings.SplitN(string(bs), ":", 2); len(strs) != 2 {
|
||||
log.Warn("[Token] basic split err, decode = %s", string(bs))
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid basic authorization")
|
||||
}
|
||||
|
||||
clientId, clientSecret := strs[0], strs[1]
|
||||
if err = db.Default.Session().Model(&model.Client{}).
|
||||
Where("client_id", clientId).
|
||||
Take(client).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: client invalid")
|
||||
}
|
||||
|
||||
log.Error("[Token] db take client by id = %s, err = %s", clientId, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
if client.ClientSecret != clientSecret {
|
||||
log.Warn("[Token] client_secret invalid, want = %s, got = %s", client.ClientSecret, clientSecret)
|
||||
return c.Status(http.StatusUnauthorized).SendString("Unauthorized: client secret invalid")
|
||||
}
|
||||
|
||||
if err = cache.Client.GetScan(tool.Timeout(2), cache.Prefix+"auth_code:"+req.Code).Scan(&opId); err != nil {
|
||||
if errors.Is(err, cache.ErrorKeyNotFound) {
|
||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid code")
|
||||
}
|
||||
|
||||
log.Error("[S] handleToken: get code from cache err = %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
op.Id = opId
|
||||
if err = db.Default.Session().Take(op).Error; err != nil {
|
||||
log.Error("[S] handleToken: get op by id err, id = %d, err = %s", opId, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
if token, err = op.JwtEncode(); err != nil {
|
||||
log.Error("[S] handleToken: encode token err, id = %d, err = %s", opId, err.Error())
|
||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
refreshToken := uuid.New().String()
|
||||
|
||||
return c.JSON(map[string]any{
|
||||
"access_token": token,
|
||||
"refresh_token": refreshToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 24 * 3600,
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user