01 前言
一种开放协议,允许以简单和标准的方法从 Web、移动和桌面应用程序进行安全授权。
https://github.com/go-oauth2/oauth2
网页授权 | 微信开发文档
https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/oauth/authorize", authorizeHandler)
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/oauth/access_token", accessTokenHandler)
mux.HandleFunc("/oauth/refresh_token", refreshTokenHandler)
mux.HandleFunc(oauth2.DefaultLoginPageUrl, loginPageHandler)
mux.HandleFunc(oauth2.DefaultAuthPageUrl, authPageHandler)
server := http.Server{
Addr: ":8088",
Handler: mux,
}
fmt.Println("Start server at http://localhost:8088/")
server.ListenAndServe()
}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h1>Login In</h1>
<form action="/login" method="POST">
<div class="form-group">
<label for="username">User Name</label>
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Auth</title>
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<form action="/oauth/authorize" method="POST">
<h1>Authorize</h1>
<p>The client would like to perform actions on your behalf.</p>
<p>
<button
type="submit"
class="btn btn-primary btn-lg"
style="width:200px;"
>
Allow
</button>
</p>
</form>
</div>
</div>
</body>
</html>
NewDefaultManager:创建一个默认的授权管理实例。
SetAuthorizeCodeTokenCfg:设置授权code的token配置参数。
SetRefreshTokenCfg:设置刷新token的配置参数。
MapTokenStorage:配置token的存储,例如使用redis。
MapAccessGenerate:配置生成 access_token 的实例。
MapClientStorage:配置客户端的存储实例。
NewServer:创建一个默认的授权服务器。
SetPasswordAuthorizationHandler:通过 username 和 password 获取 user_id。
SetUserAuthorizationHandler:从请求授权中获取用户id。
SetClientInfoHandler:从请求授权中获取client。
package oauth2
import (
"github.com/go-oauth2/oauth2/v4/manage"
"time"
)
var (
// ClusterType means redis cluster.
ClusterType = "cluster"
// NodeType means redis node.
NodeType = "node"
// DefaultAuthorizeCodeTokenCfg is the default authorization code grant token config.
DefaultAuthorizeCodeTokenCfg = &manage.Config{AccessTokenExp: time.Hour * 2, RefreshTokenExp: time.Hour * 24 * 30, IsGenerateRefresh: true}
// DefaultRefreshTokenCfg is the default refresh token config.
DefaultRefreshTokenCfg = &manage.RefreshingConfig{IsGenerateRefresh: false, IsRemoveAccess: false, IsRemoveRefreshing: false}
// DefaultTokenStoragePrefixKey is the default token storage prefix key.
DefaultTokenStoragePrefixKey = "oauth2:token:"
// DefaultRedisStorePrefixKey is the default session redis prefix key.
DefaultRedisStorePrefixKey = "oauth2:store:"
// DefaultAuthorizeForm is the default authorization form.
DefaultAuthorizeForm = "DefaultAuthorizeForm"
// DefaultLoginUserId is the default login user id.
DefaultLoginUserId = "DefaultLoginUserId"
DefaultLoginPageUrl = "/page/login"
DefaultAuthPageUrl = "/page/auth"
)
// RedisConf defines the redis configuration.
type RedisConf struct {
Addrs []string `json:"addrs"`
Pass string `json:"pass,optional"`
Type string `json:"type,default=node"`
}
package oauth2
import (
"context"
"github.com/bytedance/sonic"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
oredis "github.com/go-oauth2/redis/v4"
"github.com/go-redis/redis/v8"
sredis "github.com/go-session/redis/v3"
"github.com/go-session/session/v3"
"net/http"
)
type Server struct {
*manage.Manager
*server.Server
}
// NewServer 创建oauth2 server
func NewServer(conf RedisConf) *Server {
initSession(conf)
// 1. create to default authorization management instance.
manager := manage.NewDefaultManager()
// 1.1 set the authorization code grant token config.
manager.SetAuthorizeCodeTokenCfg(DefaultAuthorizeCodeTokenCfg)
// 1.2 set the refresh token config.
manager.SetRefreshTokenCfg(DefaultRefreshTokenCfg)
// 1.3 mapping the token store interface, set token store (redis).
if conf.Type == ClusterType {
manager.MapTokenStorage(oredis.NewRedisClusterStore(&redis.ClusterOptions{Addrs: conf.Addrs, Password: conf.Pass}, DefaultTokenStoragePrefixKey))
} else {
manager.MapTokenStorage(oredis.NewRedisStore(&redis.Options{Addr: conf.Addrs[0], Password: conf.Pass}, DefaultTokenStoragePrefixKey))
}
// 1.4 mapping the access token generate interface, generate access_token.
manager.MapAccessGenerate(generates.NewAccessGenerate())
// 1.5 mapping the client store interface, set client store.
clientStore := store.NewClientStore()
clientId, clientSecret, domain := "123456", "abcdef", "localhost:8080"
clientStore.Set("123456", &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: domain,
})
manager.MapClientStorage(clientStore)
// 2. create authorization server.
config := server.NewConfig()
config.AllowGetAccessRequest = true
server := server.NewServer(config, manager)
// server needs to implement UserAuthorizationHandler and PasswordAuthorizationHandler.
// 2.1 set the password authorization handler(get user id from username and password).
server.SetPasswordAuthorizationHandler(func(ctx context.Context, clientID, username, password string) (userId string, err error) {
return "user_id", nil
})
// 2.2 set the user authorization handler(get user id from request).
server.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (userId string, err error) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
return "", err
}
// 2.3.1 it's from /oauth/authorize?client_id=xxx and need to redirect to /login.
if r.Method == "GET" {
marshal, err := sonic.Marshal(r.Form)
if err != nil {
return "", err
}
store.Set(DefaultAuthorizeForm, string(marshal))
store.Save()
w.Header().Set("Location", DefaultLoginPageUrl)
w.WriteHeader(http.StatusFound)
return "", nil
}
// 2.3.2 it's allow auth and from /login redirect /oauth/authorize, will get code to redirect_uri.
if r.Method == "POST" {
if r.Form == nil {
err = r.ParseForm()
if err != nil {
return "", err
}
}
userID, ok := store.Get(DefaultLoginUserId)
if !ok {
// not userID in session, redirect to /login.
w.Header().Set("Location", DefaultLoginPageUrl)
w.WriteHeader(http.StatusFound)
return
}
store.Delete(DefaultLoginUserId)
store.Save()
return userID.(string), nil
}
return "", nil
})
// 2.3 set client info handler
server.SetClientInfoHandler(func(r *http.Request) (clientID, clientSecret string, err error) {
// 从请求头获取clientId、clientSecret等
clientID = r.FormValue("client_id")
if len(clientID) == 0 {
return "", "", errors.ErrInvalidRequest
}
clientSecret = r.FormValue("client_secret")
if len(clientSecret) == 0 {
return "", "", errors.ErrInvalidRequest
}
return
})
return &Server{Manager: manager, Server: server}
}
// use redis as session manager.
func initSession(conf RedisConf) {
var store session.ManagerStore
if conf.Type == ClusterType {
store = sredis.NewRedisClusterStore(&sredis.ClusterOptions{Addrs: conf.Addrs, Password: conf.Pass}, DefaultRedisStorePrefixKey)
} else {
store = sredis.NewRedisStore(&sredis.Options{Addr: conf.Addrs[0], Password: conf.Pass}, DefaultRedisStorePrefixKey)
}
session.InitManager(session.SetStore(store))
}
package types
// AuthorizeReq 用户授权请求,如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。
type AuthorizeReq struct {
ClientId string `query:"client_id"`
RedirectUri string `query:"redirect_uri"`
ResponseType string `query:"response_type,default=code"`
Scope string `query:"scope,default=scope"`
State string `query:"state,optional"`
}
type AuthorizeResp struct {
}
// LoginReq 登录请求
type LoginReq struct {
Username string `form:"username"`
Password string `form:"password"`
}
type LoginResp struct {
}
// AccessTokenReq 通过code换取access_token
type AccessTokenReq struct {
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Code string `json:"code"`
GrantType string `json:"grant_type,default=authorization_code"`
RedirectUri string `json:"redirect_uri"`
}
type AccessTokenRsp struct {
}
// RefreshTokenReq 刷新access_token
type RefreshTokenReq struct {
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RefreshToken string `json:"refresh_token"`
GrantType string `json:"grant_type,default=refresh_token"`
RedirectUri string `json:"redirect_uri"`
}
type RefreshTokenRsp struct {
}
package main
import (
"fmt"
"github.com/bytedance/sonic"
"github.com/go-session/session/v3"
"net/http"
"net/url"
"oauth2"
"oauth2/server/types"
"oauth2/server/utils"
"os"
)
var server *oauth2.Server
func init() {
server = oauth2.NewServer(oauth2.RedisConf{Addrs: []string{"127.0.0.1:6379"}})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/oauth/authorize", authorizeHandler)
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/oauth/access_token", accessTokenHandler)
mux.HandleFunc("/oauth/refresh_token", refreshTokenHandler)
mux.HandleFunc(oauth2.DefaultLoginPageUrl, loginPageHandler)
mux.HandleFunc(oauth2.DefaultAuthPageUrl, authPageHandler)
server := http.Server{
Addr: ":8088",
Handler: mux,
}
fmt.Println("Start server at http://localhost:8088/")
server.ListenAndServe()
}
// 授权接口
func authorizeHandler(w http.ResponseWriter, r *http.Request) {
// There will be two steps here, one is to enter through the /authorize interface,
// and the other is to enter when obtaining the code
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var form url.Values
v, ok := store.Get(oauth2.DefaultAuthorizeForm)
if !ok {
// oauth2 access GET /oauth/authorize?client_id=123456&redirect_uri=http://localhost:8080/redirect.html&response_type=code&scope=scope
// Do some verification, such as client_id, redirect_uri, etc.
var req types.AuthorizeReq
err = utils.Parse(r, &req)
if err != nil {
return
}
} else {
// oauth2 access POST /oauth/authorize get code to redirect_uri
err = sonic.Unmarshal([]byte(v.(string)), &form)
if err != nil {
return
}
}
r.Form = form
store.Delete(oauth2.DefaultAuthorizeForm)
store.Save()
err = server.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
// 登录接口,校验用户名密码
func loginHandler(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
err := utils.Parse(r, &req)
if err != nil {
return
}
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Form == nil {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if req.Username != "admin" || req.Password != "123456" {
w.WriteHeader(http.StatusForbidden)
return
}
store.Set(oauth2.DefaultLoginUserId, r.Form.Get("username"))
store.Save()
w.Header().Set("Location", oauth2.DefaultAuthPageUrl)
w.WriteHeader(http.StatusFound)
}
// 获取access_token
func accessTokenHandler(w http.ResponseWriter, r *http.Request) {
var req types.AccessTokenReq
err := utils.Parse(r, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
form := url.Values{}
form.Set("client_id", req.ClientId)
form.Set("client_secret", req.ClientSecret)
form.Set("code", req.Code)
form.Set("grant_type", req.GrantType)
form.Set("redirect_uri", req.RedirectUri)
r.Form = form
err = server.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
// 刷新access_token
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
var req types.RefreshTokenReq
err := utils.Parse(r, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
form := url.Values{}
form.Set("client_id", req.ClientId)
form.Set("client_secret", req.ClientSecret)
form.Set("refresh_token", req.RefreshToken)
form.Set("grant_type", req.GrantType)
form.Set("redirect_uri", req.RedirectUri)
r.Form = form
err = server.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
// 登录页面
func loginPageHandler(w http.ResponseWriter, r *http.Request) {
outputHTML(w, r, "static/login.html")
}
// 授权页面
func authPageHandler(w http.ResponseWriter, r *http.Request) {
outputHTML(w, r, "static/auth.html")
}
func outputHTML(w http.ResponseWriter, req *http.Request, filename string) {
file, err := os.Open(filename)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer file.Close()
fi, _ := file.Stat()
http.ServeContent(w, req, file.Name(), fi.ModTime(), file)
}
curl --location --request GET 'http://localhost:8088/oauth/access_token' \
--header 'Content-Type: application/json' \
--data '{
"client_id": "123456",
"client_secret": "abcdef",
"code": "ZDVMZGMWMJQTZTC0MI0ZOWJJLTG5ZTKTN2FKMJG1MJC1ODMY",
"grant_type": "authorization_code",
"redirect_uri":"http://localhost:8080/redirect.html"
}'
{
"access_token": "NJDKZGEZMJQTZMI2MC0ZY2VILTKYNWYTY2YXMGU3NDRMYZBJ",
"expires_in": 604800,
"refresh_token": "N2Q5NZCYZMMTOTVKZI01OWE3LTLIOTCTNMJHNMMWZWI2OTM5",
"scope": "scope",
"token_type": "Bearer"
}
curl --location --request GET 'http://localhost:8088/oauth/refresh_token' \
--header 'Content-Type: application/json' \
--data '{
"client_id": "123456",
"client_secret": "abcdef",
"refresh_token": "N2Q5NZCYZMMTOTVKZI01OWE3LTLIOTCTNMJHNMMWZWI2OTM5",
"grant_type": "refresh_token",
"redirect_uri":"http://localhost:8080/redirect.html"
}'
{
"access_token": "YWMXZJDKMDMTNZVINS0ZOTJLLTG4OGUTN2FKYJG2NGU4NJGX",
"expires_in": 604800,
"scope": "scope",
"token_type": "Bearer"
}
推荐阅读
欢迎关注 点赞 分享与收藏,一起学习进步!
专注于 Go 语言领域工作学习经验分享,期待与您一同学习进步。 学的不仅是技术,更是梦想!