├── .gitignore ├── README.md ├── app.js ├── bin └── www ├── config.js ├── config └── default.js ├── docs ├── api-gateway.png └── api-gateway.umd3 ├── middleware └── auth.js ├── package.json ├── test-target-service.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## api-gateway 2 | 3 | api 网关在微服务架构中非常重要,api 网关封装了所有单个服务的接口,客户端只需要和网关交互即可。api 网关会把请求分发到具体的微服务中。另外,网关还肩负了 token 校验的功能,为所有的请求增加了安全性。 4 | 5 | 本项目模拟了 3 个微服务和 1 个网关,所有请求打到网关后会转发到具体的服务中。 6 | 7 | ![](https://github.com/xugy0926/api-gateway/blob/master/docs/api-gateway.png?raw=true) 8 | 9 | ## startup 10 | 11 | #### startup target service 12 | 13 | ``` 14 | $ node test-target-service.js 15 | ``` 16 | 17 | #### test 18 | 19 | ``` 20 | $ npm test 21 | ``` 22 | 23 | #### startup api-gateway 24 | 25 | ``` 26 | $ node bin/www 27 | ``` 28 | 29 | #### test token 30 | 31 | ``` 32 | { 33 | user_id: '123', 34 | user_name: 'xugaoyang', 35 | exp: 1580515200000 // 2020/1/1 36 | } 37 | 38 | secret_key: 'secret_key' 39 | 40 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNTgwNTE1MjAwMDAwfQ.Zm0l0pNdSqszmPVcuY0dxWLA1tGhuAODgS0KOWvrVdQ 41 | ``` 42 | 43 | ``` 44 | { 45 | user_id: '123', 46 | user_name: 'xugaoyang', 47 | exp: 1485907200000 // 2017/1/1 48 | } 49 | 50 | secret_key: 'secret_key' 51 | 52 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNDg1OTA3MjAwMDAwfQ.PW5J1lCt0_2pmTHEKngk_MgvttuTmVc0f8VRUUST2i0 53 | ``` 54 | 55 | #### test request 56 | 57 | ###### /user/123/info 58 | 59 | request 60 | 61 | ``` 62 | GET /user/123/info 63 | Host: localhost:3000 64 | ``` 65 | 66 | response 67 | 68 | ``` 69 | request successfully proxied to user : /123/info 70 | { 71 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7", 72 | "accept-encoding": "gzip, deflate, br", 73 | "accept": "*/*", 74 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", 75 | "cache-control": "no-cache", 76 | "connection": "close", 77 | "host": "localhost:3000" 78 | } 79 | ``` 80 | 81 | ###### /post/123/info 82 | 83 | request 84 | 85 | ``` 86 | POST /post/123/info HTTP/1.1 87 | Host: localhost:3000 88 | x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNTgwNTE1MjAwMDAwfQ.Zm0l0pNdSqszmPVcuY0dxWLA1tGhuAODgS0KOWvrVdQ 89 | ``` 90 | 91 | response 92 | 93 | ``` 94 | request successfully proxied to post : /123/info 95 | { 96 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7", 97 | "accept-encoding": "gzip, deflate, br", 98 | "accept": "*/*", 99 | "content-type": "text/plain;charset=UTF-8", 100 | "x-access-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNTgwNTE1MjAwMDAwfQ.Zm0l0pNdSqszmPVcuY0dxWLA1tGhuAODgS0KOWvrVdQ", 101 | "origin": "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop", 102 | "cache-control": "no-cache", 103 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", 104 | "content-length": "17", 105 | "connection": "close", 106 | "host": "localhost:3000" 107 | } 108 | ``` 109 | 110 | ###### /message/123/info 111 | 112 | request 113 | 114 | ``` 115 | PUT /message/123/info 116 | Host: localhost:3000 117 | ``` 118 | 119 | response 120 | 121 | ``` 122 | request successfully proxied to message : /123/info 123 | { 124 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7", 125 | "accept-encoding": "gzip, deflate, br", 126 | "accept": "*/*", 127 | "content-type": "text/plain;charset=UTF-8", 128 | "origin": "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop", 129 | "cache-control": "no-cache", 130 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", 131 | "content-length": "17", 132 | "connection": "close", 133 | "host": "localhost:3000" 134 | } 135 | ``` 136 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const config = require('config') 2 | const express = require('express') 3 | const httpProxy = require('http-proxy') 4 | const morgan = require('morgan') 5 | 6 | const auth = require('./middleware/auth') 7 | 8 | const proxy = httpProxy.createServer() 9 | const app = express() 10 | 11 | app.use(morgan('tiny')) 12 | 13 | const goto = function (req, res, next) { 14 | proxy.web(req, res, { 15 | target: req.target 16 | }, (err) => { 17 | res.status(500).end(req.target + ' error.') 18 | }) 19 | } 20 | 21 | const userRouter = express.Router() 22 | userRouter.get('/:id/info', goto) 23 | 24 | const postRouter = express.Router() 25 | postRouter.post('/:id/info', auth, goto) 26 | 27 | const messageRouter = express.Router() 28 | messageRouter.put('/:id/info', auth, goto) 29 | 30 | const configTarget = function (target) { 31 | return function (req, res, next) { 32 | req.target = target 33 | next() 34 | } 35 | } 36 | 37 | app.use('/user', configTarget(config.get('/user')), userRouter) 38 | app.use('/post', configTarget(config.get('/post')), postRouter) 39 | app.use('/message', configTarget(config.get('/message')), messageRouter) 40 | 41 | module.exports = app 42 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | const app = require('../app') 2 | const http = require('http') 3 | 4 | app.set('port', 3000) 5 | 6 | const server = http.createServer(app) 7 | 8 | server.listen(3000) 9 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tokenTag: 'x-access-token', 3 | secretKey: 'secret-key' 4 | } -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '/user': 'http://localhost:9001', 3 | '/post': 'http://localhost:9002', 4 | '/message': 'http://localhost:9003' 5 | } 6 | -------------------------------------------------------------------------------- /docs/api-gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xugy0926/api-gateway/ea7d81bb09f6b8a7c7b70484ec548346f1b19560/docs/api-gateway.png -------------------------------------------------------------------------------- /docs/api-gateway.umd3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xugy0926/api-gateway/ea7d81bb09f6b8a7c7b70484ec548346f1b19560/docs/api-gateway.umd3 -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jwt-simple') 2 | 3 | const config = require('../config') 4 | 5 | const auth = function (req, res, next) { 6 | const token = req.headers[config.tokenTag] || '' 7 | 8 | if (!token) { 9 | res.status(401).json({ msg: 'Access token not found' }) 10 | return 11 | } 12 | 13 | try { 14 | const decoded = jwt.decode(token, config.secretKey) 15 | 16 | if (!decoded.user_id || !decoded.user_name || !decoded.exp) { 17 | res.status(401).json({ msg: 'Access token illegal' }) 18 | return 19 | } 20 | 21 | if (decoded.exp <= Date.now()) { 22 | res.status(401).json({ msg: 'Access token has expired' }) 23 | return 24 | } 25 | 26 | next() 27 | } catch (err) { 28 | res.status(401).json({ msg: 'Access token failed' }) 29 | } 30 | } 31 | 32 | module.exports = auth 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway", 3 | "version": "1.0.0", 4 | "description": "api gateway for nodejs", 5 | "main": "bin/www", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "keywords": [ 10 | "api", 11 | "gateway", 12 | "nodejs" 13 | ], 14 | "author": "xugaoyang ", 15 | "license": "ISC", 16 | "dependencies": { 17 | "config": "^2.0.1", 18 | "express": "^4.16.3", 19 | "http-proxy": "^1.17.0", 20 | "jwt-simple": "^0.5.1", 21 | "log4js": "^3.0.1", 22 | "morgan": "^1.9.0" 23 | }, 24 | "devDependencies": { 25 | "mocha": "^5.2.0", 26 | "supertest": "^3.1.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-target-service.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors') 2 | const http = require('http') 3 | const logger = require('log4js').getLogger('target-service') 4 | 5 | logger.level = 'info' 6 | // 7 | // Target Http Server 8 | // 9 | http.createServer(function (req, res) { 10 | logger.info('[user]' + '[' + req.method + '] ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 11 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 12 | res.write('request successfully proxied to user : ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 13 | res.end() 14 | }).listen(9001) 15 | 16 | // 17 | // Target Http Server 18 | // 19 | http.createServer(function (req, res) { 20 | logger.info('[post]' + '[' + req.method + '] ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 21 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 22 | res.write('request successfully proxied to post : ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 23 | res.end() 24 | }).listen(9002) 25 | 26 | // 27 | // Target Http Server 28 | // 29 | http.createServer(function (req, res) { 30 | logger.info('[message]' + '[' + req.method + '] ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 31 | res.writeHead(200, { 'Content-Type': 'text/plain' }) 32 | res.write('request successfully proxied to message : ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)) 33 | res.end() 34 | }).listen(9003) 35 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const app = require('../app') 2 | const request = require('supertest')(app) 3 | 4 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNTgwNTE1MjAwMDAwfQ.Zm0l0pNdSqszmPVcuY0dxWLA1tGhuAODgS0KOWvrVdQ' 5 | const expirationToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcl9uYW1lIjoieHVnYW95YW5nIiwiZXhwIjoxNDg1OTA3MjAwMDAwfQ.PW5J1lCt0_2pmTHEKngk_MgvttuTmVc0f8VRUUST2i0' 6 | const illegalToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwiZXhwIjoxNTgwNTE1MjAwMDAwfQ.vxJIXTp9jYqOKr0rI6MbU97Hp-kA_6BB9gunH_F0TJI' 7 | 8 | describe('api', function () { 9 | describe('GET /user/123/info', function () { 10 | it('should request user service', done => { 11 | request.get(`/user/123/info`).expect(200, done) 12 | }) 13 | }) 14 | 15 | describe('POST /post/123/info', function () { 16 | it('should request post service -- valid token', done => { 17 | request.post(`/post/123/info`) 18 | .set('x-access-token', token) 19 | .expect(200, done) 20 | }) 21 | }) 22 | 23 | describe('POST /post/123/info', function () { 24 | it('should request post service -- expiration token', done => { 25 | request.post(`/post/123/info`) 26 | .set('x-access-token', expirationToken) 27 | .expect(401, done) 28 | }) 29 | }) 30 | 31 | describe('POST /post/123/info', function () { 32 | it('should request post service -- illegal token', done => { 33 | request.post(`/post/123/info`) 34 | .set('x-access-token', illegalToken) 35 | .expect(401, done) 36 | }) 37 | }) 38 | 39 | describe('POST /post/123/info', function () { 40 | it('should request post service -- no token', done => { 41 | request.post(`/post/123/info`) 42 | .expect(401, done) 43 | }) 44 | }) 45 | 46 | describe('PUT /message/123/info', function () { 47 | it('should request message service -- valid token', done => { 48 | request.put(`/message/123/info`) 49 | .set('x-access-token', token) 50 | .expect(200, done) 51 | }) 52 | }) 53 | 54 | describe('PUT /message/123/info', function () { 55 | it('should request message service -- expiration token', done => { 56 | request.put(`/message/123/info`) 57 | .set('x-access-token', expirationToken) 58 | .expect(401, done) 59 | }) 60 | }) 61 | 62 | describe('PUT /message/123/info', function () { 63 | it('should request message service -- no token', done => { 64 | request.put(`/message/123/info`) 65 | .expect(401, done) 66 | }) 67 | }) 68 | }) 69 | --------------------------------------------------------------------------------