Kong网关(二):开启OAuth2 Plugin插件
OAuth2.0是OAuth协议的延续版本,但不向前兼容OAuth 1.0(即完全废止了OAuth1.0). OAuth 2.0关注客户端开发者的简易性. 要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限. 同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程.2012年10月,OAuth 2.0协议正式发布为RFC 6749.
1. Kong网关:术语
Kong网关(一):service/route/consumer概念理解
plugin
:一个在将请求代理到上游API之前或之后在Kong内部执行动作的插件.Service
:代表外部上游 API或微服务的Kong实体.upstream service
:这是指您位于Kong后面的自己的API/Service,客户端请求将转发到该API /Service.
2. Kong网关:安装OAuth2插件流程
2.1 添加一个测试Service
这个Service 是需要通过用过OAuth2 Token才能访问的服务. eg: 介入第三方Github登陆之后去要Github OAuth2 Token才能访问被授权的Github账号信息/repo信息.
创建一个叫作 mock-service
url为http://mockbin.org/request
的需要Oauth2 token 授权的服务.
curl -X POST \
--url "http://127.0.0.1:8001/services" \
--data "name=mock-service" \
--data "url=http://mockbin.org/request"
2.1.1 为这个服务增加route路由
这个路由/mock
类似于nginx server配置文件的的location
curl -X POST \
--url "http://127.0.0.1:8001/services/mock-service/routes" \
--data 'hosts[]=mockbin.org' \
--data 'paths[]=/mock'
2.1.2 测试这个API
curl -X GET \
--url "http://127.0.0.1:8000/mock" \
--header "Host: mockbin.org"
返回一个这个请求的全部信息
2.2 为这个服务开启OAuth2 Plugin
curl -X POST \
--url http://127.0.0.1:8001/services/mock-service/plugins/ \
--data "name=oauth2" \
--data "config.scopes=email, phone, address" \
--data "config.mandatory_scope=true" \
--data "config.enable_authorization_code=true"
response结果中包含 provision_key
, provision_key
将被使用在web应用和Kong网关之前的API通讯,来确保通讯的安全.
{
"service_id": "2c0c8c84-cd7c-40b7-c0b8-41202e5ee50b",
"value": {
"scopes": [
"email",
"phone",
"address"
],
"mandatory_scope": true,
"provision_key": "2ef290c575cc46eec61947aa9f1e67d3",
"hide_credentials": false,
"enable_authorization_code": true,
"token_expiration": 7200
},
"created_at": 1435783325000,
"enabled": true,
"name": "oauth2",
"id": "656954bd-2130-428f-c25c-8ec47227dafa"
}
现在再次发送刚才定义的Route API, 发现它现在是受到保护的.
curl -X GET \
--url "http://127.0.0.1:8000/mock" \
--header "Host: mockbin.org"
2.3 创建消费者
类似于您在Github 中注册自己账号
curl -X GET \
--url "http://127.0.0.1:8000/mock" \
--header "Host: mockbin.org"
为您的开发者账号添加一个app
curl -X POST \
--url "http://127.0.0.1:8001/consumers/thefosk/oauth2/" \
--data "name=MojoTV" \
--data "redirect_uris[]=https://mojotv.cn"
response 结果如下:
{
"consumer_id": "a0977612-bd8c-4c6f-ccea-24743112847f",
"client_id": "318f98be1453427bc2937fceab9811bd",
"id": "7ce2f90c-3ec5-4d93-cd62-3d42eb6f9b64",
"name": "MojoTV",
"created_at": 1435783376000,
"redirect_uri": "https://mojotv.cn/",
"client_secret": "efbc9e1f2bcc4968c988ef5b839dd5a4"
}
2.4 保存上面的环境变量
const PROVISION_KEY="2ef290c575cc46eec61947aa9f1e67d3"
const KONG_ADMIN="http://127.0.0.1:8001"
const KONG_API="https://127.0.0.1:8443"
const API_PATH="/mock"
const SERVICE_HOST="mockbin.org"
const SCOPES=`{
"email": "获取用户email权限",
"address": "获取用户address权限",
"phone": "获取用户phone权限"
}`
3. Golang 实现Kong 网关OAuth2 网关授权服务
按照官方node-express.js Demo实习 https://github.com/Kong/kong-oauth2-hello-world
package main
import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/url"
"strings"
)
import "net/http"
//第三方APP
//kong admin RESTful api 获取
const CLIENT_ID = "oInH5MY5H0IZVPT899zn2Fq0YLKmetsv"
const CLIENT_SECRET = "1EHqpI6MENOk8YfOEnYjd9hGrOhrPVEq"
//kong 服务配置
const PROVISION_KEY = "Y1TV0sKHqtIzZ4JHtI0x420o3PZwM6gD"
const KONG_ADMIN = "http://oauth2.mojotv.cn:8001"
const KONG_API = "https://oauth2.mojotv.cn:8443" //自定义ssl 证书 需要关闭ssl-check
const API_PATH = `/mock`
const SERVICE_HOST = "mockbin.org"
// oauth 服务器
const COOKIE_AUTH = "auth1"
var SCOPE_DESCRIPTIONS = map[string]string{
"email": "读取用户email权限",
"address": "读取用户address权限",
"phone": "读取用户phone权限",
}
func RunAuthCenter() {
// logger and recovery (crash-free) middleware
router := gin.Default()
router.Use(mwPanic)
router.LoadHTMLGlob("templates/*")
//显示前端登陆form页面
router.GET("login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", gin.H{
"client_id": c.Query("client_id"),
"response_type": c.Query("response_type"),
"scope": c.Query("scope"),
})
})
//自己用户名体系 身份验证
router.POST("login", func(c *gin.Context) {
user := c.PostForm("name")
password := c.PostForm("password")
clientId := c.PostForm("client_id")
responseType := c.PostForm("response_type")
scope := c.PostForm("authorize")
//todo::数据库比对
if user != "admin" || password != "admin" {
//登陆失败
c.SetCookie(COOKIE_AUTH, "", -1, "/", "", false, true)
loginUri := fmt.Sprintf("login?client_id=%s&response_type=%s&scope=%s", clientId, responseType, url.QueryEscape(scope))
c.Redirect(http.StatusMovedPermanently, loginUri)
return
}
//登陆成功
c.SetCookie(COOKIE_AUTH, "001", 36000000, "/", "", false, true)
if clientId != "" {
authorizationUri := fmt.Sprintf("authorize?client_id=%s&response_type=%s&scope=%s", clientId, responseType, url.QueryEscape(scope))
c.Redirect(http.StatusMovedPermanently, authorizationUri)
return
}
c.JSON(http.StatusOK, "login succeed")
})
//显示 OAuth2 用户授权页面 可以做出github 第三方网站登陆授权页面
router.GET("authorize", func(c *gin.Context) {
//todo:: html 中展示用户信息
//todo:: html 中展示权限信息
//todo:: 模仿对象 github.com 账号登陆 https://github.com/login/oauth/authorize?client_id=30d97f7383706665c5e0&scope=user%3Aemail
clientId := c.Query("client_id")
responseType := c.Query("response_type")
scope := c.Query("scope")
authedUserId, err := c.Cookie(COOKIE_AUTH)
if err != nil {
log.WithError(err).Error("auth cookie", COOKIE_AUTH)
c.Request.URL.Path = "/login"
router.HandleContext(c)
return
}
if authedUserId == "" {
log.WithError(err).Error("auth cookie empty")
c.Request.URL.Path = "/login"
router.HandleContext(c)
return
}
appInfo, err := kongApiGetAppName(clientId)
if err != nil {
c.JSON(http.StatusOK, "client id is wrong")
return
}
c.HTML(http.StatusOK, "authorization.html", gin.H{
"client_id": clientId,
"response_type": responseType,
"scope": scope,
"application_name": appInfo.Data[0].Name, //todo:: render more info to html template
"SCOPE_DESCRIPTIONS": SCOPE_DESCRIPTIONS})
})
//OAuth2 用户授权页面 点击授权按钮, 获取从kong api consumer/app redirect_url(包含authorization_code)
//第三方 app 通过 authorization_code 调用kong OAuth2接口换取 token
//使用token 访问service 的route 接口
router.POST("authorize", func(c *gin.Context) {
clientId := c.PostForm("client_id")
responseType := c.PostForm("response_type")
scope := c.PostForm("scope")
authedUserId, err := c.Cookie(COOKIE_AUTH)
if err != nil {
log.WithError(err).Error("cookie", COOKIE_AUTH)
c.JSON(http.StatusOK, "auth cookie 为空")
return
}
redirectURL := KongApiPostAuthorization(clientId, responseType, scope, authedUserId)
if redirectURL != "" {
c.Redirect(http.StatusMovedPermanently, redirectURL)
return
}
c.JSON(http.StatusOK, "没有被授权")
})
router.Run(":3000")
// router.Run(":3000") for a hard coded port
}
func main() {
RunAuthCenter()
}
type appInfo struct {
Next interface{} `json:"next"`
Data []struct {
RedirectUris []string `json:"redirect_uris"`
CreatedAt int `json:"created_at"`
Consumer struct {
ID string `json:"id"`
} `json:"consumer"`
ID string `json:"id"`
Tags interface{} `json:"tags"`
Name string `json:"name"`
ClientSecret string `json:"client_secret"`
ClientID string `json:"client_id"`
} `json:"data"`
}
func kongApiGetAppName(clientId string) (*appInfo, error) {
resp, err := http.Get(KONG_ADMIN + "/oauth2?client_id=" + clientId)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data := &appInfo{}
err = json.NewDecoder(resp.Body).Decode(data)
if err != nil {
return nil, err
}
return data, nil
}
func KongApiPostAuthorization(clientId, responseType, scope, authenticatedUserid string) string {
// disable ssl check kong API 使用的是自己颁发的SSL 证书 post请求失败, 关闭httpClient的SSL证书校验
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
payload := fmt.Sprintf(`{"client_id":"%s","response_type":"%s","scope":"%s","provision_key":"%s","authenticated_userid":"%s"}`, clientId, responseType, scope, PROVISION_KEY, authenticatedUserid)
reader := strings.NewReader(payload)
endpoint := KONG_API + API_PATH + "/oauth2/authorize"
req, err := http.NewRequest("POST", endpoint, reader)
if err != nil {
log.WithError(err).Error("new request")
return ""
}
req.Host = "mockbin.org"
req.Header.Add("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.WithError(err).Error("do request")
return ""
}
defer res.Body.Close()
data := struct {
RedirectURI string `json:"redirect_uri"`
}{}
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
log.WithError(err).Error("json decode")
return ""
}
return data.RedirectURI
}
func mwPanic(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
switch v := err.(type) {
case error:
log.WithError(v).Error("gin handle all error")
c.AbortWithStatusJSON(http.StatusOK, gin.H{"code": http.StatusNoContent, "msg": v.Error()})
//todo:: 细分更多的panic
default:
log.Error(v)
}
}
}()
c.Next()
}
func handlerError(err error) {
if err != nil {
panic(err)
}
}
Jekyll 模板语法冲突 请在Golang中把 {{ }} 斜线剔除
golang login.html 模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<h2>欢迎登陆</h2>
<form method="post" action="login">
<input type="hidden" name="client_id" value="\{\{.client_id\}\}">
<input type="hidden" name="response_type" value="\{\{.response_type\}\}">
<input type="hidden" name="scope" value="\{\{.scope\}\}">
<p>姓名:<input type="text" name="name" size="10"></p>
<p>密码:<input type="password" name="password" size="10"></p>
<p><input type="submit" value="登陆">
<input type="reset" value="取消"></p>
</form>
</body>
</html>
golang authorization.html OAuth2授权页面模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<h2>Oauth2 Authorization</h2>
<h3>\{\{.application_name\}\}</h3>
<p>
\{\{range $key,$value := .SCOPE_DESCRIPTIONS\}\}<li>\{\{ $key \}\} -- \{\{ $value\}\}</li>\{\{else\}\}<div><strong>没有数据</strong></div>\{\{end\}\}
</p>
<form method="post" action="authorize">
<input type="hidden" name="client_id" value="\{\{.client_id\}\}">
<input type="hidden" name="response_type" value="\{\{.response_type\}\}">
<input type="hidden" name="scope" value="\{\{.scope\}\}">
<p><input type="submit" value="授权">
<input type="reset" value="取消"></p>
</form>
</body>
</html>
4. 测试Kong OAuth2 服务
4.1 OAuth2授权页面
http://127.0.0.1:3000/authorize?response_type=code&scope=email%20address&client_id={$client_id}
4.2 Authorize
点击收取按之前必须用户已经登陆,
点击 授权按钮之后 301重定向到 https://mojotv.cn/?code=8Yy4LZHxsNZ9fObqoQ8D6or6MRT4Ui64
4.3 第三方APP通过code 换取token
curl -X POST \
--url "https://127.0.0.1:8443/mock/oauth2/token" \
--header "Host: mockbin.org" \
--data "grant_type=authorization_code" \
--data "client_id=318f98be1453427bc2937fceab9811bd" \
--data "client_secret=efbc9e1f2bcc4968c988ef5b839dd5a4" \
--data "redirect_uri=https://mojotv.com/" \
--data "code=8Yy4LZHxsNZ9fObqoQ8D6or6MRT4Ui64" \
--insecure
Response 结果:
{
"refresh_token": "N8YXZFNtx0onuuR7v465nVmnFN7vBKWk",
"token_type": "bearer",
"access_token": "njVmea9rlSbSUtZ2wDlHf62R7QKDgDhG",
"expires_in": 7200
}
4.4 使用Token访问API
curl -X GET \
--url "http://127.0.0.1:8000/mock" \
--header "Host: mockbin.org" \
--header "Authorization: bearer njVmea9rlSbSUtZ2wDlHf62R7QKDgDhG"
注意 Kong API 检验Token正确,就会转发额外的Header到后端业务服务器
...
"x-consumer-id": "77e3f7ca-a969-48bb-a6d0-4a104ea7ad1e",
"x-consumer-username": "thefosk",
"x-authenticated-scope": "email address",
...