目錄
- 技術(shù)棧
- 后端構(gòu)建 api
- 前端構(gòu)建 web
- 網(wǎng)關(guān)構(gòu)建 gateway
- Nginx 配置
- Dockerfile
- Lua 實(shí)現(xiàn)基于企業(yè)微信的網(wǎng)關(guān)認(rèn)證
- 使用 DockerCompose 進(jìn)行容器編排
這篇文章會(huì)介紹如何將后端、前端和網(wǎng)關(guān)通通使用 Docker 容器進(jìn)行運(yùn)行,并最終使用 DockerCompose 進(jìn)行容器編排。
技術(shù)棧
前端
后端
網(wǎng)關(guān)
- Nginx
- OpenResty
- Lua
- 企業(yè)微信
后端構(gòu)建 api
這里雖然我們寫(xiě)了 EXPOSE 4182,這個(gè)只用在測(cè)試的時(shí)候,生產(chǎn)環(huán)境實(shí)際上我們不會(huì)將后端接口端口進(jìn)行暴露,
而是通過(guò)容器間的網(wǎng)絡(luò)進(jìn)行互相訪問(wèn),以及最終會(huì)使用 Nginx 進(jìn)行轉(zhuǎn)發(fā)。
FROM golang:1.15.5
LABEL maintainer="K8sCat <k8scat@gmail.com>"
EXPOSE 4182
ENV GOPROXY=https://goproxy.cn,direct \
GO111MODULE=on
WORKDIR /go/src/github.com/k8scat/containerized-app/api
COPY . .
RUN go mod download && \
go build -o api main.go && \
chmod +x api
ENTRYPOINT [ "./api" ]
前端構(gòu)建 web
這里值得一提的是,因?yàn)榍岸丝隙〞?huì)去調(diào)用后端接口,而且這個(gè)接口地址是根據(jù)部署而改變,
所以這里我們使用了 ARG 指令進(jìn)行設(shè)置后端的接口地址,這樣我們只需要在構(gòu)建鏡像的時(shí)候傳入 --build-arg REACT_APP_BASE_URL=https://example.com/api
就可以調(diào)整后端接口地址了,而不是去改動(dòng)代碼。
還有一點(diǎn),有朋友肯定會(huì)發(fā)現(xiàn)這里同時(shí)使用到了 Entrypoint 和 CMD,這是為了可以在運(yùn)行的時(shí)候調(diào)整前端的端口,但實(shí)際上我們這里沒(méi)必要去調(diào)整,因?yàn)檫@里最終也是用 Nginx 進(jìn)行轉(zhuǎn)發(fā)。
FROM node:lts
LABEL maintainer="K8sCat <k8scat@gmail.com>"
WORKDIR /web
COPY . .
ARG REACT_APP_BASE_URL
RUN npm config set registry https://registry.npm.taobao.org && \
npm install && \
npm run build && \
npm install -g serve
ENTRYPOINT [ "serve", "-s", "build" ]
CMD [ "-l", "3214" ]
網(wǎng)關(guān)構(gòu)建 gateway
Nginx 配置
這里我們就分別設(shè)置了后端和前端的上游,然后設(shè)置 location 規(guī)則進(jìn)行轉(zhuǎn)發(fā)。
這里有幾個(gè)點(diǎn)可以說(shuō)一下:
- 通過(guò) set_by_lua 獲取容器的環(huán)境變量,最終在運(yùn)行的時(shí)候通過(guò)設(shè)置 environment 設(shè)置這些環(huán)境變量,更加靈活
- server_name 使用到了 $hostname,運(yùn)行時(shí)需要設(shè)置容器的 hostname
- ssl_certificate 和 ssl_certificate_key 不能使用變量設(shè)置
- 加載 gateway.lua 腳本實(shí)現(xiàn)企業(yè)微信的網(wǎng)關(guān)認(rèn)證
upstream web {
server ca-web:3214;
}
upstream api {
server ca-api:4182;
}
server {
set_by_lua $corp_id 'return os.getenv("CORP_ID")';
set_by_lua $agent_id 'return os.getenv("AGENT_ID")';
set_by_lua $secret 'return os.getenv("SECRET")';
set_by_lua $callback_host 'return os.getenv("CALLBACK_HOST")';
set_by_lua $callback_schema 'return os.getenv("CALLBACK_SCHEMA")';
set_by_lua $callback_uri 'return os.getenv("CALLBACK_URI")';
set_by_lua $logout_uri 'return os.getenv("LOGOUT_URI")';
set_by_lua $token_expires 'return os.getenv("TOKEN_EXPIRES")';
set_by_lua $use_secure_cookie 'return os.getenv("USE_SECURE_COOKIE")';
listen 443 ssl http2;
server_name $hostname;
resolver 8.8.8.8;
ssl_certificate /certs/cert.crt;
ssl_certificate_key /certs/cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
lua_ssl_verify_depth 2;
lua_ssl_trusted_certificate /etc/pki/tls/certs/ca-bundle.crt;
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
}
access_log logs/access_$year$month$day.log main;
error_log logs/error.log;
access_by_lua_file "/usr/local/openresty/nginx/conf/gateway.lua";
location ^~ /gateway {
root html;
index index.html index.htm;
}
location ^~ /api {
proxy_pass http://api;
proxy_read_timeout 3600;
proxy_http_version 1.1;
proxy_set_header X_FORWARDED_PROTO https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Connection "";
}
location ^~ / {
proxy_pass http://web;
proxy_read_timeout 3600;
proxy_http_version 1.1;
proxy_set_header X_FORWARDED_PROTO https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Connection "";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 80;
server_name $hostname;
location / {
rewrite ^/(.*) https://$server_name/$1 redirect;
}
}
Dockerfile
FROM openresty/openresty:1.19.3.1-centos
LABEL maintainer="K8sCat <k8scat@gmail.com>"
COPY gateway.conf /etc/nginx/conf.d/gateway.conf
COPY gateway.lua /usr/local/openresty/nginx/conf/gateway.lua
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
# Install lua-resty-http
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http
Lua 實(shí)現(xiàn)基于企業(yè)微信的網(wǎng)關(guān)認(rèn)證
這里面的一些配置參數(shù)都是通過(guò)獲取 Nginx 設(shè)置的變量。
local json = require("cjson")
local http = require("resty.http")
local uri = ngx.var.uri
local uri_args = ngx.req.get_uri_args()
local scheme = ngx.var.scheme
local corp_id = ngx.var.corp_id
local agent_id = ngx.var.agent_id
local secret = ngx.var.secret
local callback_scheme = ngx.var.callback_scheme or scheme
local callback_host = ngx.var.callback_host
local callback_uri = ngx.var.callback_uri
local use_secure_cookie = ngx.var.use_secure_cookie == "true" or false
local callback_url = callback_scheme .. "://" .. callback_host .. callback_uri
local redirect_url = callback_scheme .. "://" .. callback_host .. ngx.var.request_uri
local logout_uri = ngx.var.logout_uri or "/logout"
local token_expires = ngx.var.token_expires or "7200"
token_expires = tonumber(token_expires)
local function request_access_token(code)
local request = http.new()
request:set_timeout(7000)
local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/gettoken", {
method = "GET",
query = {
corpid = corp_id,
corpsecret = secret,
},
ssl_verify = true,
})
if not res then
return nil, (err or "access token request failed: " .. (err or "unknown reason"))
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/gettoken: " .. res.body
end
local data = json.decode(res.body)
if data["errcode"] ~= 0 then
return nil, data["errmsg"]
else
return data["access_token"]
end
end
local function request_user(access_token, code)
local request = http.new()
request:set_timeout(7000)
local res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo", {
method = "GET",
query = {
access_token = access_token,
code = code,
},
ssl_verify = true,
})
if not res then
return nil, "get profile request failed: " .. (err or "unknown reason")
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
end
local userinfo = json.decode(res.body)
if userinfo["errcode"] == 0 then
if userinfo["UserId"] then
res, err = request:request_uri("https://qyapi.weixin.qq.com/cgi-bin/user/get", {
method = "GET",
query = {
access_token = access_token,
userid = userinfo["UserId"],
},
ssl_verify = true,
})
if not res then
return nil, "get user request failed: " .. (err or "unknown reason")
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://qyapi.weixin.qq.com/cgi-bin/user/get"
end
local user = json.decode(res.body)
if user["errcode"] == 0 then
return user
else
return nil, user["errmsg"]
end
else
return nil, "UserId not exists"
end
else
return nil, userinfo["errmsg"]
end
end
local function is_authorized()
local headers = ngx.req.get_headers()
local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
local user_id = ngx.unescape_uri(ngx.var.cookie_OauthUserID or "")
local token = ngx.var.cookie_OauthAccessToken or ""
if expires == 0 and headers["OauthExpires"] then
expires = tonumber(headers["OauthExpires"])
end
if user_id:len() == 0 and headers["OauthUserID"] then
user_id = headers["OauthUserID"]
end
if token:len() == 0 and headers["OauthAccessToken"] then
token = headers["OauthAccessToken"]
end
local expect_token = callback_host .. user_id .. expires
if token == expect_token and expires then
if expires > ngx.time() then
return true
else
return false
end
else
return false
end
end
local function redirect_to_auth()
return ngx.redirect("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?" .. ngx.encode_args({
appid = corp_id,
agentid = agent_id,
redirect_uri = callback_url,
state = redirect_url
}))
end
local function authorize()
if uri ~= callback_uri then
return redirect_to_auth()
end
local code = uri_args["code"]
if not code then
ngx.log(ngx.ERR, "not received code from https://open.work.weixin.qq.com/wwopen/sso/qrConnect")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
local access_token, request_access_token_err = request_access_token(code)
if not access_token then
ngx.log(ngx.ERR, "got error during access token request: " .. request_access_token_err)
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
local user, request_user_err = request_user(access_token, code)
if not user then
ngx.log(ngx.ERR, "got error during profile request: " .. request_user_err)
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
ngx.log(ngx.ERR, "user id: " .. user["userid"])
local expires = ngx.time() + token_expires
local cookie_tail = "; version=1; path=/; Max-Age=" .. expires
if use_secure_cookie then
cookie_tail = cookie_tail .. "; secure"
end
local user_id = user["userid"]
local user_token = callback_host .. user_id .. expires
ngx.header["Set-Cookie"] = {
"OauthUserID=" .. ngx.escape_uri(user_id) .. cookie_tail,
"OauthAccessToken=" .. ngx.escape_uri(user_token) .. cookie_tail,
"OauthExpires=" .. expires .. cookie_tail,
}
return ngx.redirect(uri_args["state"])
end
local function handle_logout()
if uri == logout_uri then
ngx.header["Set-Cookie"] = "OauthAccessToken==deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
--return ngx.redirect("/")
end
end
handle_logout()
if (not is_authorized()) then
authorize()
end
使用 DockerCompose 進(jìn)行容器編排
這里需要講幾個(gè)點(diǎn):
- 設(shè)置前端的 args 可以在前端構(gòu)建時(shí)傳入后端接口地址
- 設(shè)置網(wǎng)關(guān)的 hostname 可以設(shè)置網(wǎng)關(guān)容器的 hostname
- 設(shè)置網(wǎng)關(guān)的 environment 可以傳入相關(guān)配置
- 最終運(yùn)行時(shí)只有網(wǎng)關(guān)層進(jìn)行暴露端口
version: "3.8"
services:
api:
build: ./api
image: ca-api:latest
container_name: ca-api
web:
build:
context: ./web
args:
REACT_APP_BASE_URL: https://example.com/api
image: ca-web:latest
container_name: ca-web
gateway:
build: ./gateway
image: ca-gateway:latest
hostname: example.com
volumes:
- ./gateway/certs/fullchain.pem:/certs/cert.crt
- ./gateway/certs/privkey.pem:/certs/cert.key
ports:
- 80:80
- 443:443
environment:
- CORP_ID=
- AGENT_ID=
- SECRET=
- CALLBACK_HOST=example.com
- CALLBACK_SCHEMA=https
- CALLBACK_URI=/gateway/oauth_wechat
- LOGOUT_URI=/gateway/oauth_logout
- TOKEN_EXPIRES=7200
- USE_SECURE_COOKIE=true
container_name: ca-gateway
開(kāi)源代碼
GitHub https://github.com/k8scat/containerized-app
Gitee https://gitee.com/k8scat/containerized-app
到此這篇關(guān)于Docker+DockerCompose封裝web應(yīng)用的文章就介紹到這了,更多相關(guān)Docker+DockerCompose封裝web應(yīng)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!