添加 OIDC 和 OAuth2 服务器的基础结构,包括配置、数据库模型、服务、处理器和路由。新增登录页面模板,支持用户认证和授权流程。
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
data/oidc_server.db
|
||||
46
config/config.go
Normal file
46
config/config.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
} `yaml:"server"`
|
||||
|
||||
Database struct {
|
||||
Type string `yaml:"type"`
|
||||
Path string `yaml:"path"`
|
||||
} `yaml:"database"`
|
||||
|
||||
OAuth struct {
|
||||
IssuerURL string `yaml:"issuer_url"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
RedirectURL string `yaml:"redirect_url"`
|
||||
} `yaml:"oauth"`
|
||||
|
||||
JWT struct {
|
||||
SigningKey string `yaml:"signing_key"`
|
||||
} `yaml:"jwt"`
|
||||
}
|
||||
|
||||
var GlobalConfig Config
|
||||
|
||||
func Init() error {
|
||||
configFile, err := os.ReadFile("config/config.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(configFile, &GlobalConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
config/config.yaml
Normal file
16
config/config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 8080
|
||||
host: "localhost"
|
||||
|
||||
database:
|
||||
type: "sqlite"
|
||||
path: "data/oidc_server.db"
|
||||
|
||||
oauth:
|
||||
issuer_url: "http://localhost:8080"
|
||||
client_id: "default_client"
|
||||
client_secret: "default_secret"
|
||||
redirect_url: "http://localhost:8080/callback"
|
||||
|
||||
jwt:
|
||||
signing_key: "your-secret-key-here"
|
||||
49
go.mod
Normal file
49
go.mod
Normal file
@@ -0,0 +1,49 @@
|
||||
module oidc-oauth2-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
112
go.sum
Normal file
112
go.sum
Normal file
@@ -0,0 +1,112 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
|
||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
81
handlers/auth.go
Normal file
81
handlers/auth.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"oidc-oauth2-server/services"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
type LoginData struct {
|
||||
Error string
|
||||
RedirectURI string
|
||||
State string
|
||||
ClientID string
|
||||
ResponseType string
|
||||
Scope string
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowLogin 显示登录页面
|
||||
func (h *AuthHandler) ShowLogin(c *gin.Context) {
|
||||
data := LoginData{
|
||||
RedirectURI: c.Query("redirect_uri"),
|
||||
State: c.Query("state"),
|
||||
ClientID: c.Query("client_id"),
|
||||
ResponseType: c.Query("response_type"),
|
||||
Scope: c.Query("scope"),
|
||||
}
|
||||
c.HTML(http.StatusOK, "login.html", data)
|
||||
}
|
||||
|
||||
// HandleLogin 处理登录请求
|
||||
func (h *AuthHandler) HandleLogin(c *gin.Context) {
|
||||
username := c.PostForm("username")
|
||||
password := c.PostForm("password")
|
||||
|
||||
user, err := h.authService.Authenticate(username, password)
|
||||
if err != nil {
|
||||
data := LoginData{
|
||||
Error: "用户名或密码错误",
|
||||
RedirectURI: c.PostForm("redirect_uri"),
|
||||
State: c.PostForm("state"),
|
||||
ClientID: c.PostForm("client_id"),
|
||||
ResponseType: c.PostForm("response_type"),
|
||||
Scope: c.PostForm("scope"),
|
||||
}
|
||||
c.HTML(http.StatusOK, "login.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置用户会话
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", user.ID)
|
||||
session.Save()
|
||||
|
||||
// 重定向回授权页面
|
||||
redirectURI := c.PostForm("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
redirectURI = "/authorize"
|
||||
}
|
||||
|
||||
query := c.Request.URL.Query()
|
||||
query.Set("client_id", c.PostForm("client_id"))
|
||||
query.Set("response_type", c.PostForm("response_type"))
|
||||
query.Set("scope", c.PostForm("scope"))
|
||||
query.Set("state", c.PostForm("state"))
|
||||
query.Set("redirect_uri", redirectURI)
|
||||
|
||||
c.Redirect(http.StatusFound, "/authorize?"+query.Encode())
|
||||
}
|
||||
171
handlers/oidc.go
Normal file
171
handlers/oidc.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oidc-oauth2-server/models"
|
||||
"oidc-oauth2-server/services"
|
||||
)
|
||||
|
||||
type OIDCHandler struct {
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
config *OIDCConfig
|
||||
oauthService *services.OAuthService
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||
JwksURI string `json:"jwks_uri"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ClaimsSupported []string `json:"claims_supported"`
|
||||
}
|
||||
|
||||
func NewOIDCHandler(issuerURL string, oauthService *services.OAuthService, authService *services.AuthService) *OIDCHandler {
|
||||
config := &OIDCConfig{
|
||||
Issuer: issuerURL,
|
||||
AuthorizationEndpoint: issuerURL + "/authorize",
|
||||
TokenEndpoint: issuerURL + "/token",
|
||||
UserinfoEndpoint: issuerURL + "/userinfo",
|
||||
JwksURI: issuerURL + "/jwks",
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
ScopesSupported: []string{"openid", "profile", "email"},
|
||||
ClaimsSupported: []string{"sub", "name", "email", "email_verified"},
|
||||
}
|
||||
|
||||
return &OIDCHandler{
|
||||
config: config,
|
||||
oauthService: oauthService,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// OpenIDConfiguration handles /.well-known/openid-configuration endpoint
|
||||
func (h *OIDCHandler) OpenIDConfiguration(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, h.config)
|
||||
}
|
||||
|
||||
// Authorize handles /authorize endpoint
|
||||
func (h *OIDCHandler) Authorize(c *gin.Context) {
|
||||
// 检查用户是否已登录
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
if userID == nil {
|
||||
// 用户未登录,重定向到登录页面
|
||||
query := c.Request.URL.Query()
|
||||
c.Redirect(http.StatusFound, "/login?"+query.Encode())
|
||||
return
|
||||
}
|
||||
|
||||
req := &services.AuthorizeRequest{
|
||||
ResponseType: c.Query("response_type"),
|
||||
ClientID: c.Query("client_id"),
|
||||
RedirectURI: c.Query("redirect_uri"),
|
||||
Scope: c.Query("scope"),
|
||||
State: c.Query("state"),
|
||||
}
|
||||
|
||||
if err := h.oauthService.ValidateAuthorizeRequest(req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
authCode, err := h.oauthService.GenerateAuthorizationCode(userID.(uint), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization code"})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建重定向 URL
|
||||
redirectURL, _ := url.Parse(req.RedirectURI)
|
||||
q := redirectURL.Query()
|
||||
q.Set("code", authCode.Code)
|
||||
if req.State != "" {
|
||||
q.Set("state", req.State)
|
||||
}
|
||||
redirectURL.RawQuery = q.Encode()
|
||||
|
||||
c.Redirect(http.StatusFound, redirectURL.String())
|
||||
}
|
||||
|
||||
// Token handles /token endpoint
|
||||
func (h *OIDCHandler) Token(c *gin.Context) {
|
||||
req := &services.TokenRequest{
|
||||
GrantType: c.PostForm("grant_type"),
|
||||
Code: c.PostForm("code"),
|
||||
RedirectURI: c.PostForm("redirect_uri"),
|
||||
ClientID: c.PostForm("client_id"),
|
||||
ClientSecret: c.PostForm("client_secret"),
|
||||
}
|
||||
|
||||
if req.GrantType != "authorization_code" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_grant_type"})
|
||||
return
|
||||
}
|
||||
|
||||
tokenResponse, err := h.oauthService.ExchangeToken(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tokenResponse)
|
||||
}
|
||||
|
||||
// Userinfo handles /userinfo endpoint
|
||||
func (h *OIDCHandler) Userinfo(c *gin.Context) {
|
||||
// 从 token 中获取用户 ID
|
||||
userID := c.GetUint("user_id")
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
var user models.User
|
||||
if err := h.authService.GetUserByID(userID, &user); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user_not_found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取授权范围
|
||||
scope := c.GetString("scope")
|
||||
scopes := strings.Split(scope, " ")
|
||||
|
||||
// 准备返回的声明
|
||||
claims := gin.H{
|
||||
"sub": fmt.Sprintf("%d", user.ID),
|
||||
}
|
||||
|
||||
// 根据授权范围添加相应的声明
|
||||
for _, s := range scopes {
|
||||
switch s {
|
||||
case "profile":
|
||||
claims["name"] = user.Username
|
||||
case "email":
|
||||
claims["email"] = user.Email
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, claims)
|
||||
}
|
||||
|
||||
// JWKS handles /jwks endpoint
|
||||
func (h *OIDCHandler) JWKS(c *gin.Context) {
|
||||
// TODO: 实现 JWKS 密钥集获取逻辑
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Not implemented yet"})
|
||||
}
|
||||
78
main.go
Normal file
78
main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oidc-oauth2-server/config"
|
||||
"oidc-oauth2-server/handlers"
|
||||
"oidc-oauth2-server/models"
|
||||
"oidc-oauth2-server/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化配置
|
||||
if err := config.Init(); err != nil {
|
||||
log.Fatalf("Failed to initialize config: %v", err)
|
||||
}
|
||||
|
||||
// 初始化数据库连接
|
||||
db, err := gorm.Open(sqlite.Open(config.GlobalConfig.Database.Path), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// 运行数据库迁移
|
||||
if err := models.AutoMigrate(db); err != nil {
|
||||
log.Fatalf("Failed to run database migrations: %v", err)
|
||||
}
|
||||
|
||||
// 初始化服务
|
||||
authService := services.NewAuthService(db)
|
||||
oauthService := services.NewOAuthService(db, []byte(config.GlobalConfig.JWT.SigningKey))
|
||||
|
||||
// 设置 Gin 路由
|
||||
r := gin.Default()
|
||||
|
||||
// 设置模板目录
|
||||
r.LoadHTMLGlob("templates/*")
|
||||
|
||||
// 设置 session 中间件
|
||||
store := cookie.NewStore([]byte("secret"))
|
||||
r.Use(sessions.Sessions("oidc_session", store))
|
||||
|
||||
// 健康检查
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
// 创建处理器
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
oidcHandler := handlers.NewOIDCHandler(config.GlobalConfig.OAuth.IssuerURL, oauthService, authService)
|
||||
|
||||
// 认证路由
|
||||
r.GET("/login", authHandler.ShowLogin)
|
||||
r.POST("/login", authHandler.HandleLogin)
|
||||
|
||||
// OIDC 端点
|
||||
r.GET("/.well-known/openid-configuration", oidcHandler.OpenIDConfiguration)
|
||||
r.GET("/authorize", oidcHandler.Authorize)
|
||||
r.POST("/token", oidcHandler.Token)
|
||||
r.GET("/userinfo", oidcHandler.Userinfo)
|
||||
r.GET("/jwks", oidcHandler.JWKS)
|
||||
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf("%s:%d", config.GlobalConfig.Server.Host, config.GlobalConfig.Server.Port)
|
||||
log.Printf("Starting server on %s", addr)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
59
middleware/auth.go
Normal file
59
middleware/auth.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
func BearerAuth(jwtSecret []byte) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Bearer 前缀
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
|
||||
// 解析和验证令牌
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// 验证签名算法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将令牌中的声明存储在上下文中
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
c.Set("user_id", uint(claims["sub"].(float64)))
|
||||
c.Set("scope", claims["scope"].(string))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
22
models/authorization_code.go
Normal file
22
models/authorization_code.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthorizationCode struct {
|
||||
gorm.Model
|
||||
Code string `gorm:"uniqueIndex;not null"`
|
||||
ClientID string `gorm:"not null"`
|
||||
UserID uint `gorm:"not null"`
|
||||
RedirectURI string `gorm:"not null"`
|
||||
Scope string `gorm:"not null"`
|
||||
ExpiresAt time.Time `gorm:"not null"`
|
||||
Used bool `gorm:"default:false"`
|
||||
}
|
||||
|
||||
func (ac *AuthorizationCode) TableName() string {
|
||||
return "oauth_authorization_codes"
|
||||
}
|
||||
23
models/client.go
Normal file
23
models/client.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
gorm.Model
|
||||
ClientID string `gorm:"uniqueIndex;not null"`
|
||||
ClientSecret string `gorm:"not null"`
|
||||
RedirectURIs []string `gorm:"type:json"`
|
||||
GrantTypes []string `gorm:"type:json"`
|
||||
Scopes []string `gorm:"type:json"`
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (c *Client) TableName() string {
|
||||
return "oauth_clients"
|
||||
}
|
||||
14
models/migration.go
Normal file
14
models/migration.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AutoMigrate 自动迁移数据库表结构
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&User{},
|
||||
&Client{},
|
||||
&AuthorizationCode{},
|
||||
)
|
||||
}
|
||||
22
models/user.go
Normal file
22
models/user.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `gorm:"uniqueIndex;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
Email string `gorm:"uniqueIndex"`
|
||||
LastLogin time.Time
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (u *User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
3
readme.md
Normal file
3
readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# OIDC-OATH-SERVER
|
||||
|
||||
一个标准的 OIDC 和 OAUTH2 服务端,提供简单的账号密码登录
|
||||
79
services/auth.go
Normal file
79
services/auth.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oidc-oauth2-server/models"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAuthService(db *gorm.DB) *AuthService {
|
||||
return &AuthService{db: db}
|
||||
}
|
||||
|
||||
func (s *AuthService) Authenticate(username, password string) (*models.User, error) {
|
||||
user := &models.User{}
|
||||
if err := s.db.Where("username = ?", username).First(user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.LastLogin = time.Now()
|
||||
s.db.Save(user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) CreateUser(username, password, email string) (*models.User, error) {
|
||||
// 检查用户名是否已存在
|
||||
var count int64
|
||||
s.db.Model(&models.User{}).Where("username = ?", username).Count(&count)
|
||||
if count > 0 {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Username: username,
|
||||
Password: string(hashedPassword),
|
||||
Email: email,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据用户 ID 获取用户信息
|
||||
func (s *AuthService) GetUserByID(id uint, user *models.User) error {
|
||||
result := s.db.First(user, id)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
return result.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
186
services/oauth.go
Normal file
186
services/oauth.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oidc-oauth2-server/models"
|
||||
)
|
||||
|
||||
type OAuthService struct {
|
||||
db *gorm.DB
|
||||
jwtSecret []byte
|
||||
tokenTTL time.Duration
|
||||
}
|
||||
|
||||
type AuthorizeRequest struct {
|
||||
ResponseType string
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
Scope string
|
||||
State string
|
||||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
GrantType string
|
||||
Code string
|
||||
RedirectURI string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
}
|
||||
|
||||
func NewOAuthService(db *gorm.DB, jwtSecret []byte) *OAuthService {
|
||||
return &OAuthService{
|
||||
db: db,
|
||||
jwtSecret: jwtSecret,
|
||||
tokenTTL: time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OAuthService) ValidateAuthorizeRequest(req *AuthorizeRequest) error {
|
||||
if req.ResponseType != "code" {
|
||||
return errors.New("unsupported response type")
|
||||
}
|
||||
|
||||
client := &models.Client{}
|
||||
if err := s.db.First(client, "client_id = ?", req.ClientID).Error; err != nil {
|
||||
return errors.New("invalid client")
|
||||
}
|
||||
|
||||
// 验证重定向 URI
|
||||
validRedirect := false
|
||||
for _, uri := range client.RedirectURIs {
|
||||
if uri == req.RedirectURI {
|
||||
validRedirect = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validRedirect {
|
||||
return errors.New("invalid redirect URI")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) GenerateAuthorizationCode(userID uint, req *AuthorizeRequest) (*models.AuthorizationCode, error) {
|
||||
// 生成随机授权码
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
authCode := &models.AuthorizationCode{
|
||||
Code: code,
|
||||
ClientID: req.ClientID,
|
||||
RedirectURI: req.RedirectURI,
|
||||
UserID: userID,
|
||||
Scope: req.Scope,
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute),
|
||||
Used: false,
|
||||
}
|
||||
|
||||
// 保存授权码到数据库
|
||||
if err := s.db.Create(authCode).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authCode, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) ExchangeToken(req *TokenRequest) (*TokenResponse, error) {
|
||||
// 验证授权码
|
||||
authCode := &models.AuthorizationCode{}
|
||||
if err := s.db.Where("code = ? AND client_id = ? AND used = ?",
|
||||
req.Code, req.ClientID, false).First(authCode).Error; err != nil {
|
||||
return nil, errors.New("invalid authorization code")
|
||||
}
|
||||
|
||||
// 验证授权码是否过期
|
||||
if time.Now().After(authCode.ExpiresAt) {
|
||||
return nil, errors.New("authorization code expired")
|
||||
}
|
||||
|
||||
// 验证重定向 URI
|
||||
if authCode.RedirectURI != req.RedirectURI {
|
||||
return nil, errors.New("redirect URI mismatch")
|
||||
}
|
||||
|
||||
// 验证客户端
|
||||
client := &models.Client{}
|
||||
if err := s.db.Where("client_id = ? AND client_secret = ?",
|
||||
req.ClientID, req.ClientSecret).First(client).Error; err != nil {
|
||||
return nil, errors.New("invalid client credentials")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user := &models.User{}
|
||||
if err := s.db.First(user, authCode.UserID).Error; err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// 生成访问令牌
|
||||
accessToken, err := s.generateAccessToken(user, client, authCode.Scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成 ID 令牌
|
||||
idToken, err := s.generateIDToken(user, client, authCode.Scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 标记授权码为已使用
|
||||
authCode.Used = true
|
||||
s.db.Save(authCode)
|
||||
|
||||
return &TokenResponse{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(s.tokenTTL.Seconds()),
|
||||
IDToken: idToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) generateAccessToken(user *models.User, client *models.Client, scope string) (string, error) {
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"exp": now.Add(s.tokenTTL).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"iss": client.ClientID,
|
||||
"scope": scope,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.jwtSecret)
|
||||
}
|
||||
|
||||
func (s *OAuthService) generateIDToken(user *models.User, client *models.Client, scope string) (string, error) {
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"exp": now.Add(s.tokenTTL).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"iss": client.ClientID,
|
||||
"email": user.Email,
|
||||
"email_verified": true,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.jwtSecret)
|
||||
}
|
||||
87
templates/login.html
Normal file
87
templates/login.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>登录 - OIDC 服务</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.login-container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2 style="text-align: center; margin-bottom: 30px;">登录</h2>
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}">
|
||||
<input type="hidden" name="state" value="{{.State}}">
|
||||
<input type="hidden" name="client_id" value="{{.ClientID}}">
|
||||
<input type="hidden" name="response_type" value="{{.ResponseType}}">
|
||||
<input type="hidden" name="scope" value="{{.Scope}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user