# Conflicts: # doc/docs/_sidebar.md # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/HttpTool.java # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/RouteDefinition.java # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ServiceInfo.java # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ServiceInstance.java # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/RegistryService.java # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/impl/RegistryServiceNacosImpl.java # sop-admin/sop-admin-server/src/main/resources/public/static/css/app.c6dfb7ee.css # sop-admin/sop-admin-server/src/main/resources/public/static/css/chunk-2c1f2e8f.0314067f.css # sop-admin/sop-admin-server/src/main/resources/public/static/css/chunk-4de1c2b6.a37cd815.css # sop-admin/sop-admin-server/src/main/resources/public/static/css/chunk-510c5a69.5e48e29a.css # sop-admin/sop-admin-server/src/main/resources/public/static/css/chunk-c3ce42fe.6b789903.css # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-2d2085ef.2e46d1f4.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-2d221c34.0db10350.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-4de1c2b6.0188bd8f.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-510c5a69.c3dd05bb.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-6f78c9fe.bec8af48.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-9f479afe.6b795525.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-elementUI.298ac98c.js # sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-libs.75deb05f.js # sop-auth/.gitignore # sop-auth/pom.xml # sop-auth/readme.md # sop-auth/src/main/java/com/gitee/sop/sopauth/SopAuthApplication.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/AccessToken.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/AppIdManager.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/FetchTokenParam.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/FetchTokenResult.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/OAuth2Config.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/OAuth2Manager.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/OAuth2Service.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/OpenUser.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/RefreshToken.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/TokenPair.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/exception/LoginErrorException.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/impl/AppIdManagerImpl.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/impl/OAuth2ManagerCache.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/impl/OAuth2ManagerRedis.java # sop-auth/src/main/java/com/gitee/sop/sopauth/auth/impl/OAuth2ServiceImpl.java # sop-auth/src/main/java/com/gitee/sop/sopauth/config/OpenServiceConfig.java # sop-auth/src/main/java/com/gitee/sop/sopauth/controller/CallbackController.java # sop-auth/src/main/java/com/gitee/sop/sopauth/controller/OAuth2Controller.java # sop-auth/src/main/java/com/gitee/sop/sopauth/entity/IsvInfo.java # sop-auth/src/main/java/com/gitee/sop/sopauth/entity/UserInfo.java # sop-auth/src/main/java/com/gitee/sop/sopauth/mapper/IsvInfoMapper.java # sop-auth/src/main/java/com/gitee/sop/sopauth/mapper/UserInfoMapper.java # sop-auth/src/main/resources/application.properties # sop-auth/src/main/resources/templates/oauth2login.html # sop-auth/src/test/java/com/gitee/sop/sopauth/RefreshTokenTest.java # sop-auth/src/test/java/com/gitee/sop/sopauth/SopAuthApplicationTests.java # sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/GatewayFilterDefinition.java # sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RouteDefinition.java # sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/filter/PreEnvGrayFilter.java # sop-example/sop-easyopen/src/main/resources/application.properties # sop-example/sop-story/sop-story-web/src/main/java/com/gitee/sop/storyweb/controller/result/FileUploadResult.java # sop-website/.gitignore # sop-website/src/main/java/com/gitee/sop/websiteserver/WebsiteServerApplication.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/DocInfo.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/DocItem.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/DocModule.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/DocParameter.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/DocParserContext.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/EurekaApplication.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/EurekaApplications.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/EurekaApps.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/EurekaInstance.java # sop-website/src/main/java/com/gitee/sop/websiteserver/bean/EurekaUri.java # sop-website/src/main/java/com/gitee/sop/websiteserver/config/CorsConfig.java # sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java # sop-website/src/main/java/com/gitee/sop/websiteserver/controller/DocController.java # sop-website/src/main/java/com/gitee/sop/websiteserver/controller/SandboxController.java # sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManager.java # sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocParser.java # sop-website/src/main/java/com/gitee/sop/websiteserver/manager/EasyopenDocParser.java # sop-website/src/main/java/com/gitee/sop/websiteserver/manager/SwaggerDocParser.java # sop-website/src/main/java/com/gitee/sop/websiteserver/sign/AlipayApiException.java # sop-website/src/main/java/com/gitee/sop/websiteserver/sign/AlipayConstants.java # sop-website/src/main/java/com/gitee/sop/websiteserver/sign/AlipaySignature.java # sop-website/src/main/java/com/gitee/sop/websiteserver/sign/StreamUtil.java # sop-website/src/main/java/com/gitee/sop/websiteserver/sign/StringUtils.java # sop-website/src/main/java/com/gitee/sop/websiteserver/util/UploadUtil.java # sop-website/src/main/java/com/gitee/sop/websiteserver/vo/DocBaseInfoVO.java # sop-website/src/main/java/com/gitee/sop/websiteserver/vo/DocInfoVO.java # sop-website/src/main/java/com/gitee/sop/websiteserver/vo/ServiceInfoVO.java # sop-website/src/main/resources/api.json # sop-website/src/main/resources/public/assets/css/global.css # sop-website/src/main/resources/public/assets/image/auth.png # sop-website/src/main/resources/public/assets/js/format.js # sop-website/src/main/resources/public/assets/js/inputcache.js # sop-website/src/main/resources/public/assets/lib/jquery/3.2.1/jquery.min.js # sop-website/src/main/resources/public/assets/lib/layui/css/layui.css # sop-website/src/main/resources/public/assets/lib/layui/css/layui.mobile.css # sop-website/src/main/resources/public/assets/lib/layui/css/modules/code.css # sop-website/src/main/resources/public/assets/lib/layui/css/modules/laydate/default/laydate.css # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/icon-ext.png # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/icon.png # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/layer.css # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/loading-0.gif # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/loading-1.gif # sop-website/src/main/resources/public/assets/lib/layui/css/modules/layer/default/loading-2.gif # sop-website/src/main/resources/public/assets/lib/layui/font/iconfont.eot # sop-website/src/main/resources/public/assets/lib/layui/font/iconfont.svg # sop-website/src/main/resources/public/assets/lib/layui/font/iconfont.ttf # sop-website/src/main/resources/public/assets/lib/layui/font/iconfont.woff # sop-website/src/main/resources/public/assets/lib/layui/images/face/0.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/1.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/10.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/11.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/12.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/13.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/14.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/15.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/16.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/17.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/18.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/19.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/2.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/20.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/21.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/22.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/23.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/24.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/25.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/26.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/27.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/28.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/29.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/3.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/30.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/31.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/32.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/33.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/34.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/35.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/36.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/37.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/38.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/39.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/4.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/40.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/41.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/42.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/43.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/44.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/45.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/46.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/47.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/48.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/49.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/5.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/50.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/51.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/52.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/53.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/54.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/55.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/56.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/57.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/58.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/59.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/6.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/60.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/61.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/62.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/63.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/64.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/65.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/66.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/67.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/68.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/69.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/7.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/70.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/71.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/8.gif # sop-website/src/main/resources/public/assets/lib/layui/images/face/9.gif # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/carousel.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/code.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/colorpicker.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/element.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/flow.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/form.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/jquery.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/laydate.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/layedit.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/layer.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/laypage.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/laytpl.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/mobile.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/rate.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/slider.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/table.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/tree.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/upload.js # sop-website/src/main/resources/public/assets/lib/layui/lay/modules/util.js # sop-website/src/main/resources/public/assets/lib/layui/layui.all.js # sop-website/src/main/resources/public/assets/lib/layui/layui.js # sop-website/src/main/resources/public/assets/lib/layuiext/Form.js # sop-website/src/main/resources/public/assets/lib/layuiext/module/treetable-lay/treetable.css # sop-website/src/main/resources/public/assets/lib/layuiext/module/treetable-lay/treetable.js # sop-website/src/main/resources/public/config/config.js # sop-website/src/main/resources/public/index.html # sop-website/src/main/resources/public/pages/doc/auth.html # sop-website/src/main/resources/public/pages/doc/code.html # sop-website/src/main/resources/public/pages/doc/doc.html # sop-website/src/main/resources/public/pages/doc/doc.js # sop-website/src/main/resources/public/pages/doc/docEvent.js # sop-website/src/main/resources/public/pages/doc/sign.html # sop-website/src/main/resources/public/pages/sandbox/sandbox.html # sop-website/src/main/resources/public/pages/sandbox/sandbox.js # sop-website/src/test/java/com/gitee/sop/websiteserver/WebsiteServerApplicationTests.javapull/1/head
commit
6f880a3a51
@ -1,32 +1,32 @@ |
||||
* [首页](/?t=1567561041568) |
||||
* [首页](/?t=1566794701229) |
||||
* 开发文档 |
||||
* [快速体验](files/10010_快速体验.md?t=1567561041571) |
||||
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1567561041592) |
||||
* [新增接口](files/10020_新增接口.md?t=1567561041592) |
||||
* [开发流程](files/10021_开发流程.md?t=1567561041592) |
||||
* [业务参数校验](files/10030_业务参数校验.md?t=1567561041592) |
||||
* [错误处理](files/10040_错误处理.md?t=1567561041592) |
||||
* [编写文档](files/10041_编写文档.md?t=1567561041592) |
||||
* [接口交互详解](files/10050_接口交互详解.md?t=1567561041593) |
||||
* [easyopen支持](files/10070_easyopen支持.md?t=1567561041593) |
||||
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1567561041593) |
||||
* [ISV管理](files/10085_ISV管理.md?t=1567561041593) |
||||
* [自定义返回结果](files/10087_自定义返回结果.md?t=1567561041593) |
||||
* [自定义过滤器](files/10088_自定义过滤器.md?t=1567561041593) |
||||
* [路由授权](files/10090_路由授权.md?t=1567561041594) |
||||
* [接口限流](files/10092_接口限流.md?t=1567561041594) |
||||
* [监控日志](files/10093_监控日志.md?t=1567561041594) |
||||
* [SDK开发](files/10095_SDK开发.md?t=1567561041594) |
||||
* [使用SpringCloudGateway](files/10096_使用SpringCloudGateway.md?t=1567561041594) |
||||
* [应用授权](files/10097_应用授权.md?t=1567561041594) |
||||
* [传统web开发](files/10100_传统web开发.md?t=1567561041594) |
||||
* [文件上传](files/10104_文件上传.md?t=1567561041594) |
||||
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1567561041594) |
||||
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1567561041595) |
||||
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1567561041595) |
||||
* [快速体验](files/10010_快速体验.md?t=1566794701233) |
||||
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1566794701264) |
||||
* [新增接口](files/10020_新增接口.md?t=1566794701265) |
||||
* [开发流程](files/10021_开发流程.md?t=1566794701265) |
||||
* [业务参数校验](files/10030_业务参数校验.md?t=1566794701265) |
||||
* [错误处理](files/10040_错误处理.md?t=1566794701265) |
||||
* [编写文档](files/10041_编写文档.md?t=1566794701265) |
||||
* [接口交互详解](files/10050_接口交互详解.md?t=1566794701265) |
||||
* [easyopen支持](files/10070_easyopen支持.md?t=1566794701265) |
||||
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1566794701266) |
||||
* [ISV管理](files/10085_ISV管理.md?t=1566794701266) |
||||
* [自定义返回结果](files/10087_自定义返回结果.md?t=1566794701266) |
||||
* [自定义过滤器](files/10088_自定义过滤器.md?t=1566794701266) |
||||
* [路由授权](files/10090_路由授权.md?t=1566794701266) |
||||
* [接口限流](files/10092_接口限流.md?t=1566794701266) |
||||
* [监控日志](files/10093_监控日志.md?t=1566794701266) |
||||
* [SDK开发](files/10095_SDK开发.md?t=1566794701266) |
||||
* [使用SpringCloudGateway](files/10096_使用SpringCloudGateway.md?t=1566794701267) |
||||
* [应用授权](files/10097_应用授权.md?t=1566794701267) |
||||
* [传统web开发](files/10100_传统web开发.md?t=1566794701267) |
||||
* [文件上传](files/10104_文件上传.md?t=1566794701270) |
||||
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1566794701270) |
||||
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1566794701270) |
||||
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1566794701270) |
||||
* 原理分析 |
||||
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1567561041595) |
||||
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1567561041595) |
||||
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1567561041595) |
||||
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1567561041595) |
||||
* [常见问题](files/90100_常见问题.md?t=1567561041595) |
||||
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1566794701270) |
||||
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1566794701270) |
||||
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1566794701270) |
||||
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1566794701270) |
||||
* [常见问题](files/90100_常见问题.md?t=1566794701270) |
||||
|
@ -0,0 +1,10 @@ |
||||
# 开发流程 |
||||
|
||||
- 启动SOP,注册中心(nacos)、网关(sop-gateway)、后台管理(sop-admin)、文档(sop-website) |
||||
|
||||
以上服务是固定的,启动一次即可,后续不用做改动。 |
||||
|
||||
- 在微服务端开发接口,编写swagger注解文档 |
||||
- 【可选】编写sdk |
||||
- 接口开发完成,启动微服务,注册到注册中心。 |
||||
- ISV调用你刚写的接口 |
@ -0,0 +1,63 @@ |
||||
# 原理分析之如何存储路由 |
||||
|
||||
SOP基于spring cloud,因此会涉及到网关路由。但是开发者不用去配置文件定义路由的隐射关系,SOP自动帮你解决了这个问题。 |
||||
|
||||
## 获取路由信息 |
||||
|
||||
首先明确一点,路由信息由各微服务提供,因此网关需要从注册中心获取各微服务实例,这个通过nacos提供的`watch`来实现。 |
||||
当有新的微服务加入时,Nacos会触发一个事件推送,详见`NacosWatch.java`类 |
||||
|
||||
```java |
||||
this.publisher.publishEvent(new HeartbeatEvent(this, this.nacosWatchIndex.getAndIncrement())); |
||||
``` |
||||
|
||||
因此,只要注册了`HeartbeatEvent`事件就能随时感知到微服务实例的变化。如何注册呢,config类实现`ApplicationListener`接口即可。 |
||||
|
||||
```java |
||||
public class AbstractConfiguration implements ApplicationContextAware, ApplicationListener<HeartbeatEvent> { |
||||
/** |
||||
* nacos事件监听,每次微服务变化会触发这个方法 |
||||
* @see org.springframework.cloud.alibaba.nacos.discovery.NacosWatch NacosWatch |
||||
* @param heartbeatEvent |
||||
*/ |
||||
@Override |
||||
public void onApplicationEvent(HeartbeatEvent heartbeatEvent) { |
||||
...这里加载路由信息 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
然后,每个微服务提供一个restful接口,接口地址为:`http://ip:port/sop/routes`,用来返回自身路由信息。网关请求这个接口用来获取路由信息。 |
||||
|
||||
加载路由伪代码如下: |
||||
|
||||
```java |
||||
public void onApplicationEvent(HeartbeatEvent heartbeatEvent) { |
||||
// 获取nacos中的服务实例列表 |
||||
List<Instance> allInstances = namingService.getAllInstances(serviceName); |
||||
for(Instance instance : allInstances) { |
||||
// 微服务提供的接口 |
||||
String url = "http://" + instance.getIp + ":" + instance.getPort + "/sop/routes"; |
||||
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); |
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) { |
||||
// 返回来的路由信息 |
||||
String body = responseEntity.getBody(); |
||||
...加载路由到本地 |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
完整代码可参看`com.gitee.sop.gatewaycommon.manager.ServiceRoutesLoader.java` |
||||
|
||||
路由的存储方式是一个Map,key为路由id,即接口名+版本号。 |
||||
|
||||
```java |
||||
/** |
||||
* key:nameVersion |
||||
*/ |
||||
private Map<String, ZuulTargetRoute> nameVersionTargetRouteMap; |
||||
``` |
||||
|
||||
因为客户端调用接口都会传递一个接口名和版本号,因此通过这两个字段能够很快查询出路由信息,进行路由转发操作。 |
||||
|
@ -0,0 +1,437 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
import lombok.Data; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
import okhttp3.Cookie; |
||||
import okhttp3.CookieJar; |
||||
import okhttp3.FormBody; |
||||
import okhttp3.HttpUrl; |
||||
import okhttp3.MediaType; |
||||
import okhttp3.MultipartBody; |
||||
import okhttp3.OkHttpClient; |
||||
import okhttp3.Request; |
||||
import okhttp3.RequestBody; |
||||
import okhttp3.Response; |
||||
import okhttp3.ResponseBody; |
||||
import org.apache.commons.codec.digest.DigestUtils; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.Serializable; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* http请求工具,基于OKHTTP3 |
||||
* @author tanghc |
||||
*/ |
||||
public class HttpTool { |
||||
private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); |
||||
|
||||
private Map<String, List<Cookie>> cookieStore = new HashMap<String, List<Cookie>>(); |
||||
|
||||
private OkHttpClient httpClient; |
||||
|
||||
public HttpTool() { |
||||
this(new HttpToolConfig()); |
||||
} |
||||
|
||||
public HttpTool(HttpToolConfig httpToolConfig) { |
||||
this.initHttpClient(httpToolConfig); |
||||
} |
||||
|
||||
protected void initHttpClient(HttpToolConfig httpToolConfig) { |
||||
httpClient = new OkHttpClient.Builder() |
||||
// 设置链接超时时间,默认10秒
|
||||
.connectTimeout(httpToolConfig.connectTimeoutSeconds, TimeUnit.SECONDS) |
||||
.readTimeout(httpToolConfig.readTimeoutSeconds, TimeUnit.SECONDS) |
||||
.writeTimeout(httpToolConfig.writeTimeoutSeconds, TimeUnit.SECONDS) |
||||
.cookieJar(new CookieJar() { |
||||
@Override |
||||
public void saveFromResponse(HttpUrl httpUrl, List<Cookie> list) { |
||||
cookieStore.put(httpUrl.host(), list); |
||||
} |
||||
|
||||
@Override |
||||
public List<Cookie> loadForRequest(HttpUrl httpUrl) { |
||||
List<Cookie> cookies = cookieStore.get(httpUrl.host()); |
||||
return cookies != null ? cookies : new ArrayList<Cookie>(); |
||||
} |
||||
}).build(); |
||||
} |
||||
|
||||
@Data |
||||
public static class HttpToolConfig { |
||||
/** |
||||
* 请求超时时间 |
||||
*/ |
||||
private int connectTimeoutSeconds = 10; |
||||
/** |
||||
* http读取超时时间 |
||||
*/ |
||||
private int readTimeoutSeconds = 10; |
||||
/** |
||||
* http写超时时间 |
||||
*/ |
||||
private int writeTimeoutSeconds = 10; |
||||
} |
||||
|
||||
/** |
||||
* get请求 |
||||
* |
||||
* @param url |
||||
* @param header |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String get(String url, Map<String, String> header) throws IOException { |
||||
Request.Builder builder = new Request.Builder().url(url).get(); |
||||
// 添加header
|
||||
addHeader(builder, header); |
||||
|
||||
Request request = builder.build(); |
||||
Response response = httpClient.newCall(request).execute(); |
||||
return response.body().string(); |
||||
} |
||||
|
||||
/** |
||||
* 提交表单 |
||||
* |
||||
* @param url url |
||||
* @param form 参数 |
||||
* @param header header |
||||
* @param method 请求方式,post,get等 |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String request(String url, Map<String, ?> form, Map<String, String> header, HTTPMethod method) throws IOException { |
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, method); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
Response response = httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 请求json数据,contentType=application/json |
||||
* @param url 请求路径 |
||||
* @param json json数据 |
||||
* @param header header |
||||
* @return 返回响应结果 |
||||
* @throws IOException |
||||
*/ |
||||
public String requestJson(String url, String json, Map<String, String> header) throws IOException { |
||||
RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, json); |
||||
Request.Builder requestBuilder = new Request.Builder() |
||||
.url(url) |
||||
.post(body); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
Response response = httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
public static Request.Builder buildRequestBuilder(String url, Map<String, ?> form, HTTPMethod method) { |
||||
switch (method) { |
||||
case GET: |
||||
return new Request.Builder() |
||||
.url(buildHttpUrl(url, form)) |
||||
.get(); |
||||
case HEAD: |
||||
return new Request.Builder() |
||||
.url(buildHttpUrl(url, form)) |
||||
.head(); |
||||
case PUT: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.put(buildFormBody(form)); |
||||
case DELETE: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.delete(buildFormBody(form)); |
||||
default: |
||||
return new Request.Builder() |
||||
.url(url) |
||||
.post(buildFormBody(form)); |
||||
} |
||||
} |
||||
|
||||
public static HttpUrl buildHttpUrl(String url, Map<String, ?> form) { |
||||
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); |
||||
for (Map.Entry<String, ?> entry : form.entrySet()) { |
||||
urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
return urlBuilder.build(); |
||||
} |
||||
|
||||
public static FormBody buildFormBody(Map<String, ?> form) { |
||||
FormBody.Builder paramBuilder = new FormBody.Builder(StandardCharsets.UTF_8); |
||||
for (Map.Entry<String, ?> entry : form.entrySet()) { |
||||
paramBuilder.add(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
return paramBuilder.build(); |
||||
} |
||||
|
||||
/** |
||||
* 提交表单,并且上传文件 |
||||
* |
||||
* @param url |
||||
* @param form |
||||
* @param header |
||||
* @param files |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public String requestFile(String url, Map<String, ?> form, Map<String, String> header, List<UploadFile> files) |
||||
throws IOException { |
||||
// 创建MultipartBody.Builder,用于添加请求的数据
|
||||
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder(); |
||||
bodyBuilder.setType(MultipartBody.FORM); |
||||
|
||||
for (UploadFile uploadFile : files) { |
||||
// 请求的名字
|
||||
bodyBuilder.addFormDataPart(uploadFile.getName(), |
||||
// 文件的文字,服务器端用来解析的
|
||||
uploadFile.getFileName(), |
||||
// 创建RequestBody,把上传的文件放入
|
||||
RequestBody.create(null, uploadFile.getFileData()) |
||||
); |
||||
} |
||||
|
||||
for (Map.Entry<String, ?> entry : form.entrySet()) { |
||||
bodyBuilder.addFormDataPart(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
|
||||
RequestBody requestBody = bodyBuilder.build(); |
||||
|
||||
Request.Builder builder = new Request.Builder().url(url).post(requestBody); |
||||
|
||||
// 添加header
|
||||
addHeader(builder, header); |
||||
|
||||
Request request = builder.build(); |
||||
Response response = httpClient.newCall(request).execute(); |
||||
try { |
||||
return response.body().string(); |
||||
} finally { |
||||
response.close(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 请求数据 |
||||
* |
||||
* @param url 请求url |
||||
* @param form 请求数据 |
||||
* @param header header |
||||
* @return 返回Response |
||||
* @throws IOException |
||||
*/ |
||||
public Response requestForResponse(String url, Map<String, ?> form, Map<String, String> header, HTTPMethod method) throws IOException { |
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, method); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
return httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
} |
||||
|
||||
/** |
||||
* 下载文件 |
||||
* |
||||
* @param url 请求url |
||||
* @param form 请求数据 |
||||
* @param header header |
||||
* @return 返回文件流 |
||||
* @throws IOException |
||||
*/ |
||||
public InputStream downloadFile(String url, Map<String, ?> form, Map<String, String> header) throws IOException { |
||||
Request.Builder requestBuilder = buildRequestBuilder(url, form, HTTPMethod.GET); |
||||
// 添加header
|
||||
addHeader(requestBuilder, header); |
||||
|
||||
Request request = requestBuilder.build(); |
||||
Response response = httpClient |
||||
.newCall(request) |
||||
.execute(); |
||||
if (response.isSuccessful()) { |
||||
ResponseBody body = response.body(); |
||||
return body == null ? null : body.byteStream(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private void addHeader(Request.Builder builder, Map<String, String> header) { |
||||
if (header != null) { |
||||
Set<Map.Entry<String, String>> entrySet = header.entrySet(); |
||||
for (Map.Entry<String, String> entry : entrySet) { |
||||
builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void setCookieStore(Map<String, List<Cookie>> cookieStore) { |
||||
this.cookieStore = cookieStore; |
||||
} |
||||
|
||||
public void setHttpClient(OkHttpClient httpClient) { |
||||
this.httpClient = httpClient; |
||||
} |
||||
|
||||
public enum HTTPMethod { |
||||
/** http GET */ |
||||
GET, |
||||
/** http POST */ |
||||
POST, |
||||
/** http PUT */ |
||||
PUT, |
||||
/** http HEAD */ |
||||
HEAD, |
||||
/** http DELETE */ |
||||
DELETE; |
||||
|
||||
private HTTPMethod() { |
||||
} |
||||
|
||||
public String value() { |
||||
return this.name(); |
||||
} |
||||
|
||||
public static HTTPMethod fromValue(String v) { |
||||
return valueOf(v.toUpperCase()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 文件上传类 |
||||
* @author tanghc |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
public static class UploadFile implements Serializable { |
||||
private static final long serialVersionUID = -1100614660944996398L; |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param file 文件 |
||||
* @throws IOException |
||||
*/ |
||||
public UploadFile(String name, File file) throws IOException { |
||||
this(name, file.getName(), FileUtil.toBytes(file)); |
||||
} |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param fileName 文件名 |
||||
* @param input 文件流 |
||||
* @throws IOException |
||||
*/ |
||||
public UploadFile(String name, String fileName, InputStream input) throws IOException { |
||||
this(name, fileName, FileUtil.toBytes(input)); |
||||
} |
||||
|
||||
/** |
||||
* @param name 表单名称,不能重复 |
||||
* @param fileName 文件名 |
||||
* @param fileData 文件数据 |
||||
*/ |
||||
public UploadFile(String name, String fileName, byte[] fileData) { |
||||
super(); |
||||
this.name = name; |
||||
this.fileName = fileName; |
||||
this.fileData = fileData; |
||||
this.md5 = DigestUtils.md5Hex(fileData); |
||||
} |
||||
|
||||
private String name; |
||||
private String fileName; |
||||
private byte[] fileData; |
||||
private String md5; |
||||
|
||||
} |
||||
|
||||
public static class FileUtil { |
||||
|
||||
/** |
||||
* The default buffer size to use. |
||||
*/ |
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; |
||||
private static final int EOF = -1; |
||||
|
||||
/** |
||||
* 将文件流转换成byte[] |
||||
* @param input |
||||
* @return |
||||
* @throws IOException |
||||
*/ |
||||
public static byte[] toBytes(InputStream input) throws IOException { |
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(); |
||||
int n = 0; |
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; |
||||
|
||||
while (EOF != (n = input.read(buffer))) { |
||||
output.write(buffer, 0, n); |
||||
} |
||||
return output.toByteArray(); |
||||
} |
||||
|
||||
/** |
||||
* 将文件转换成数据流 |
||||
* @param file 文件 |
||||
* @return 返回数据流 |
||||
* @throws IOException |
||||
*/ |
||||
public static byte[] toBytes(File file) throws IOException { |
||||
if (file.exists()) { |
||||
if (file.isDirectory()) { |
||||
throw new IOException("File '" + file + "' exists but is a directory"); |
||||
} |
||||
if (file.canRead() == false) { |
||||
throw new IOException("File '" + file + "' cannot be read"); |
||||
} |
||||
} else { |
||||
throw new FileNotFoundException("File '" + file + "' does not exist"); |
||||
} |
||||
InputStream input = null; |
||||
try { |
||||
input = new FileInputStream(file); |
||||
return toBytes(input); |
||||
} finally { |
||||
try { |
||||
if (input != null) { |
||||
input.close(); |
||||
} |
||||
} catch (IOException ioe) { |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class NacosConfigs { |
||||
|
||||
public static final String GROUP_CHANNEL = "sop:channel"; |
||||
|
||||
public static final String GROUP_ROUTE = "sop:route"; |
||||
|
||||
public static final String DATA_ID_GRAY = "com.gitee.sop.channel.gray"; |
||||
|
||||
public static final String DATA_ID_IP_BLACKLIST = "com.gitee.sop.channel.ipblacklist"; |
||||
|
||||
public static final String DATA_ID_ISV = "com.gitee.sop.channel.isv"; |
||||
|
||||
public static final String DATA_ID_ROUTE_PERMISSION = "com.gitee.sop.channel.routepermission"; |
||||
|
||||
public static final String DATA_ID_LIMIT_CONFIG = "com.gitee.sop.channel.limitconfig"; |
||||
|
||||
public static final String DATA_ID_ROUTE_CONFIG = "com.gitee.sop.channel.routeconfig"; |
||||
|
||||
private static final String DATA_ID_TPL = "com.gitee.sop.route.%s"; |
||||
|
||||
public static String getRouteDataId(String serviceId) { |
||||
return String.format(DATA_ID_TPL, serviceId); |
||||
} |
||||
} |
@ -0,0 +1,89 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import com.gitee.easyopen.doc.annotation.ApiDocField; |
||||
import lombok.Data; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class RouteDefinition { |
||||
/** |
||||
* 路由的Id |
||||
*/ |
||||
@ApiDocField(description = "路由id") |
||||
private String id = ""; |
||||
|
||||
/** |
||||
* 接口名 |
||||
*/ |
||||
private String name; |
||||
|
||||
/** |
||||
* 版本号 |
||||
*/ |
||||
private String version; |
||||
|
||||
/** |
||||
* 路由断言集合配置 |
||||
*/ |
||||
@JSONField(serialize = false) |
||||
private List<GatewayPredicateDefinition> predicates = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由过滤器集合配置 |
||||
*/ |
||||
@JSONField(serialize = false) |
||||
private List<GatewayFilterDefinition> filters = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由规则转发的目标uri |
||||
*/ |
||||
@ApiDocField(description = "uri") |
||||
private String uri; |
||||
|
||||
/** |
||||
* uri后面跟的path |
||||
*/ |
||||
@ApiDocField(description = "path") |
||||
private String path; |
||||
|
||||
/** |
||||
* 路由执行的顺序 |
||||
*/ |
||||
private int order = 0; |
||||
|
||||
/** |
||||
* 是否忽略验证,业务参数验证除外 |
||||
*/ |
||||
@ApiDocField(description = "是否忽略验证,业务参数验证除外,1:忽略,0:不忽略") |
||||
private int ignoreValidate; |
||||
|
||||
/** |
||||
* 状态,0:待审核,1:启用,2:禁用 |
||||
*/ |
||||
@ApiDocField(description = "状态,0:待审核,1:启用,2:禁用") |
||||
private int status = 1; |
||||
|
||||
/** |
||||
* 合并结果,统一格式输出 |
||||
*/ |
||||
@ApiDocField(description = "合并结果,统一格式输出,1:合并,0:不合并") |
||||
private int mergeResult = 1; |
||||
|
||||
/** |
||||
* 是否需要授权才能访问 |
||||
*/ |
||||
@ApiDocField(description = "是否需要授权才能访问,1:是,0:否") |
||||
private int permission; |
||||
|
||||
/** |
||||
* 是否是自定义路由 |
||||
*/ |
||||
@ApiDocField(description = "是否是自定义路由,1:是,0:否") |
||||
private int custom; |
||||
} |
@ -0,0 +1,16 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class ServiceInfo { |
||||
/** 服务名称 */ |
||||
private String serviceId; |
||||
/** 实例列表 */ |
||||
private List<ServiceInstance> instances; |
||||
} |
@ -0,0 +1,48 @@ |
||||
package com.gitee.sop.adminserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class ServiceInstance { |
||||
/** |
||||
* 实例id |
||||
*/ |
||||
private String instanceId; |
||||
|
||||
/** |
||||
* 服务名称 |
||||
*/ |
||||
private String serviceId; |
||||
|
||||
/** |
||||
* ip |
||||
*/ |
||||
private String ip; |
||||
|
||||
/** |
||||
* port |
||||
*/ |
||||
private int port; |
||||
|
||||
/** |
||||
* 服务状态,UP:已上线,OUT_OF_SERVICE:已下线 |
||||
*/ |
||||
private String status; |
||||
|
||||
/** |
||||
* 更新时间 |
||||
*/ |
||||
private String updateTime; |
||||
|
||||
/** |
||||
* user extended attributes |
||||
*/ |
||||
private Map<String, String> metadata = new HashMap<String, String>(); |
||||
|
||||
} |
@ -0,0 +1,32 @@ |
||||
package com.gitee.sop.adminserver.service; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.nacos.api.annotation.NacosInjected; |
||||
import com.alibaba.nacos.api.config.ConfigService; |
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.gitee.sop.adminserver.bean.ChannelMsg; |
||||
import com.gitee.sop.adminserver.common.BizException; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
public class ConfigPushService { |
||||
|
||||
@NacosInjected |
||||
private ConfigService configService; |
||||
|
||||
public void publishConfig(String dataId, String groupId, ChannelMsg channelMsg) { |
||||
try { |
||||
log.info("nacos配置, dataId={}, groupId={}, operation={}", dataId, groupId, channelMsg.getOperation()); |
||||
configService.publishConfig(dataId, groupId, JSON.toJSONString(channelMsg)); |
||||
} catch (NacosException e) { |
||||
log.error("nacos配置失败, dataId={}, groupId={}, operation={}", dataId, groupId, channelMsg.getOperation(), e); |
||||
throw new BizException("nacos配置失败"); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,50 @@ |
||||
package com.gitee.sop.adminserver.service; |
||||
|
||||
import com.gitee.sop.adminserver.bean.ServiceInfo; |
||||
import com.gitee.sop.adminserver.bean.ServiceInstance; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface RegistryService { |
||||
String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss"; |
||||
|
||||
/** |
||||
* 获取所有服务列表 |
||||
* |
||||
* @param pageNo 当前页码 |
||||
* @param pageSize 分页大小 |
||||
* @return 返回服务列表 |
||||
* @throws Exception 获取失败抛出异常 |
||||
*/ |
||||
List<ServiceInfo> listAllService(int pageNo, int pageSize) throws Exception; |
||||
|
||||
/** |
||||
* 服务上线 |
||||
* |
||||
* @param serviceInstance |
||||
* @throws Exception 服务上线失败抛出异常 |
||||
*/ |
||||
void onlineInstance(ServiceInstance serviceInstance) throws Exception; |
||||
|
||||
/** |
||||
* 服务下线 |
||||
* |
||||
* @param serviceInstance |
||||
* @throws Exception 服务下线失败抛出异常 |
||||
*/ |
||||
void offlineInstance(ServiceInstance serviceInstance) throws Exception; |
||||
|
||||
|
||||
/** |
||||
* 设置实例元数据 |
||||
* |
||||
* @param serviceInstance 实例 |
||||
* @param key key |
||||
* @param value 值 |
||||
* @throws Exception 设置实例元数据失败抛出异常 |
||||
*/ |
||||
void setMetadata(ServiceInstance serviceInstance, String key, String value) throws Exception; |
||||
} |
@ -0,0 +1,134 @@ |
||||
package com.gitee.sop.adminserver.service.impl; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.alibaba.nacos.api.naming.NamingFactory; |
||||
import com.alibaba.nacos.api.naming.NamingService; |
||||
import com.alibaba.nacos.api.naming.pojo.Instance; |
||||
import com.alibaba.nacos.api.naming.pojo.ListView; |
||||
import com.gitee.sop.adminserver.bean.HttpTool; |
||||
import com.gitee.sop.adminserver.bean.ServiceInfo; |
||||
import com.gitee.sop.adminserver.bean.ServiceInstance; |
||||
import com.gitee.sop.adminserver.service.RegistryService; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
import javax.annotation.PostConstruct; |
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* nacos接口实现, https://nacos.io/zh-cn/docs/open-api.html
|
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class RegistryServiceNacosImpl implements RegistryService { |
||||
|
||||
private static HttpTool httpTool = new HttpTool(); |
||||
|
||||
@Value("${nacos.discovery.server-addr:${registry.nacos-server-addr:}}") |
||||
private String nacosAddr; |
||||
|
||||
private NamingService namingService; |
||||
|
||||
@PostConstruct |
||||
public void after() throws NacosException { |
||||
if (StringUtils.isBlank(nacosAddr)) { |
||||
throw new IllegalArgumentException("请在配置文件中指定nacos.discovery.server-addr参数"); |
||||
} |
||||
namingService = NamingFactory.createNamingService(nacosAddr); |
||||
} |
||||
|
||||
@Override |
||||
public List<ServiceInfo> listAllService(int pageNo, int pageSize) throws Exception { |
||||
ListView<String> servicesOfServer = namingService.getServicesOfServer(pageNo, pageSize); |
||||
List<String> serverList = servicesOfServer.getData(); |
||||
List<ServiceInfo> serviceInfoList = new ArrayList<>(); |
||||
for (String serviceName : serverList) { |
||||
ServiceInfo serviceInfo = new ServiceInfo(); |
||||
serviceInfo.setServiceId(serviceName); |
||||
List<Instance> instanceList = namingService.getAllInstances(serviceName); |
||||
if (CollectionUtils.isEmpty(instanceList)) { |
||||
serviceInfo.setInstances(Collections.emptyList()); |
||||
} else { |
||||
serviceInfo.setInstances(new ArrayList<>(instanceList.size())); |
||||
for (Instance instance : instanceList) { |
||||
ServiceInstance serviceInstance = new ServiceInstance(); |
||||
serviceInstance.setInstanceId(instance.getInstanceId()); |
||||
serviceInstance.setServiceId(serviceInfo.getServiceId()); |
||||
serviceInstance.setIp(instance.getIp()); |
||||
serviceInstance.setPort(instance.getPort()); |
||||
boolean isOnline = instance.getWeight() > 0; |
||||
String status = isOnline ? "UP" : "OUT_OF_SERVICE"; |
||||
serviceInstance.setStatus(status); |
||||
serviceInstance.setMetadata(instance.getMetadata()); |
||||
serviceInfo.getInstances().add(serviceInstance); |
||||
} |
||||
} |
||||
serviceInfoList.add(serviceInfo); |
||||
} |
||||
serviceInfoList.sort((o1, o2) -> o2.getInstances().size() - o1.getInstances().size()); |
||||
return serviceInfoList; |
||||
} |
||||
|
||||
@Override |
||||
public void onlineInstance(ServiceInstance serviceInstance) throws Exception { |
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance); |
||||
// 上线,把权重设置成1
|
||||
instance.setWeight(1); |
||||
this.updateInstance(instance); |
||||
} |
||||
|
||||
@Override |
||||
public void offlineInstance(ServiceInstance serviceInstance) throws Exception { |
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance); |
||||
// 下线,把权重设置成0
|
||||
instance.setWeight(0); |
||||
this.updateInstance(instance); |
||||
} |
||||
|
||||
@Override |
||||
public void setMetadata(ServiceInstance serviceInstance, String key, String value) throws Exception { |
||||
// 查询实例
|
||||
Instance instance = this.getInstance(serviceInstance); |
||||
// 设置metadata
|
||||
Map<String, String> metadata = instance.getMetadata(); |
||||
metadata.put(key, value); |
||||
this.updateInstance(instance); |
||||
} |
||||
|
||||
protected void updateInstance(Instance instance) throws IOException { |
||||
String json = JSON.toJSONString(instance); |
||||
JSONObject jsonObject = JSON.parseObject(json); |
||||
String response = httpTool.request("http://" + nacosAddr + "/nacos/v1/ns/instance", jsonObject, null, HttpTool.HTTPMethod.PUT); |
||||
if (!"ok".equalsIgnoreCase(response)) { |
||||
throw new RuntimeException(response); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 查询实例 |
||||
* @param serviceInstance 实例信息 |
||||
* @return 返回nacos实例 |
||||
* @throws IOException 查询异常 |
||||
*/ |
||||
protected Instance getInstance(ServiceInstance serviceInstance) throws IOException { |
||||
Map<String, String> params = new HashMap<>(8); |
||||
params.put("serviceName", serviceInstance.getServiceId()); |
||||
params.put("ip", serviceInstance.getIp()); |
||||
params.put("port", String.valueOf(serviceInstance.getPort())); |
||||
String instanceJson = httpTool.request("http://" + nacosAddr + "/nacos/v1/ns/instance", params, null, HttpTool.HTTPMethod.GET); |
||||
Instance instance = JSON.parseObject(instanceJson, Instance.class); |
||||
instance.setServiceName(serviceInstance.getServiceId()); |
||||
return instance; |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
||||
@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#fff;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;box-shadow:inset 0 0 0 1000px #283443!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container[data-v-0204d890]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-0204d890]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-0204d890]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-0204d890]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-0204d890]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-0204d890]{position:relative}.login-container .title-container .title[data-v-0204d890]{font-size:26px;color:#eee;margin:0 auto 40px auto;text-align:center;font-weight:700}.login-container .show-pwd[data-v-0204d890]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} |
@ -0,0 +1 @@ |
||||
.wscn-http404-container[data-v-c095f994]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-c095f994]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-c095f994]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-c095f994]{width:100%}.wscn-http404 .pic-404__child[data-v-c095f994]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-c095f994]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-c095f994;animation-name:cloudLeft-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-c095f994]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-c095f994;animation-name:cloudMid-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-c095f994]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-c095f994;animation-name:cloudRight-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-c095f994{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-c095f994{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-c095f994{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-c095f994{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-c095f994{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-c095f994{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-c095f994]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-c095f994]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-c095f994],.wscn-http404 .bullshit__oops[data-v-c095f994]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-c095f994;animation-name:slideUp-data-v-c095f994;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-c095f994]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-c095f994]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-c095f994],.wscn-http404 .bullshit__return-home[data-v-c095f994]{opacity:0;-webkit-animation-name:slideUp-data-v-c095f994;animation-name:slideUp-data-v-c095f994;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-c095f994]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-c095f994{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-c095f994{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}} |
@ -0,0 +1 @@ |
||||
.custom-tree-node{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;font-size:14px;padding-right:8px}.el-input.is-disabled .el-input__inner,.el-radio__input.is-disabled+span.el-radio__label{color:#909399}.limit-tip[data-v-385a3259]{cursor:pointer;margin-left:10px} |
@ -0,0 +1 @@ |
||||
.dashboard-container[data-v-b4bd5162]{margin:30px}.dashboard-text[data-v-b4bd5162]{font-size:18px;line-height:46px} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d221c34"],{cc75:function(a,t,e){"use strict";e.r(t);var i=function(){var a=this,t=a.$createElement,e=a._self._c||t;return e("div",{staticClass:"app-container"},[e("el-form",{staticClass:"demo-form-inline",attrs:{inline:!0,model:a.searchFormData,size:"mini"}},[e("el-form-item",{attrs:{label:"IP"}},[e("el-input",{staticStyle:{width:"250px"},attrs:{clearable:!0,placeholder:"输入IP"},model:{value:a.searchFormData.ip,callback:function(t){a.$set(a.searchFormData,"ip",t)},expression:"searchFormData.ip"}})],1),a._v(" "),e("el-form-item",[e("el-button",{attrs:{type:"primary",icon:"el-icon-search"},on:{click:a.loadTable}},[a._v("查询")])],1)],1),a._v(" "),e("el-button",{staticStyle:{"margin-bottom":"10px"},attrs:{type:"primary",size:"mini",icon:"el-icon-plus"},on:{click:a.onAdd}},[a._v("新增IP")]),a._v(" "),e("el-table",{attrs:{data:a.pageInfo.rows,border:"","highlight-current-row":""}},[e("el-table-column",{attrs:{prop:"ip",label:"IP",width:"200"}}),a._v(" "),e("el-table-column",{attrs:{prop:"remark",label:"备注",width:"300"}}),a._v(" "),e("el-table-column",{attrs:{prop:"gmtCreate",label:"添加时间",width:"160"}}),a._v(" "),e("el-table-column",{attrs:{prop:"gmtModified",label:"修改时间",width:"160"}}),a._v(" "),e("el-table-column",{attrs:{label:"操作",width:"150"},scopedSlots:a._u([{key:"default",fn:function(t){return[e("el-button",{attrs:{type:"text",size:"mini"},on:{click:function(e){return a.onTableUpdate(t.row)}}},[a._v("修改")]),a._v(" "),e("el-button",{attrs:{type:"text",size:"mini"},on:{click:function(e){return a.onTableDelete(t.row)}}},[a._v("删除")])]}}])})],1),a._v(" "),e("el-pagination",{staticStyle:{"margin-top":"5px"},attrs:{background:"","current-page":a.searchFormData.pageIndex,"page-size":a.searchFormData.pageSize,"page-sizes":[5,10,20,40],total:a.pageInfo.total,layout:"total, sizes, prev, pager, next"},on:{"size-change":a.onSizeChange,"current-change":a.onPageIndexChange}}),a._v(" "),e("el-dialog",{attrs:{title:a.dialogTitle,visible:a.dialogVisible,"close-on-click-modal":!1},on:{"update:visible":function(t){a.dialogVisible=t},close:function(t){return a.resetForm("dialogForm")}}},[e("el-form",{ref:"dialogForm",attrs:{rules:a.dialogFormRules,model:a.dialogFormData,"label-width":"120px",size:"mini"}},[e("el-form-item",{attrs:{prop:"ip",label:"IP"}},[e("el-input",{directives:[{name:"show",rawName:"v-show",value:0===a.dialogFormData.id,expression:"dialogFormData.id === 0"}],model:{value:a.dialogFormData.ip,callback:function(t){a.$set(a.dialogFormData,"ip",t)},expression:"dialogFormData.ip"}}),a._v(" "),e("span",{directives:[{name:"show",rawName:"v-show",value:a.dialogFormData.id>0,expression:"dialogFormData.id > 0"}]},[a._v(a._s(a.dialogFormData.ip))])],1),a._v(" "),e("el-form-item",{attrs:{prop:"remark",label:"备注"}},[e("el-input",{model:{value:a.dialogFormData.remark,callback:function(t){a.$set(a.dialogFormData,"remark",t)},expression:"dialogFormData.remark"}})],1)],1),a._v(" "),e("div",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[e("el-button",{on:{click:function(t){a.dialogVisible=!1}}},[a._v("取 消")]),a._v(" "),e("el-button",{attrs:{type:"primary"},on:{click:a.onDialogSave}},[a._v("保 存")])],1)],1)],1)},o=[],l={data:function(){var a=/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/,t=function(t,e,i){""===e?i(new Error("请输入IP")):(a.test(e)||i(new Error("IP格式不正确")),i())};return{searchFormData:{ip:"",pageIndex:1,pageSize:10},pageInfo:{rows:[],total:0},dialogVisible:!1,dialogTitle:"",dialogFormData:{id:0,ip:"",remark:""},dialogFormRules:{ip:[{validator:t,trigger:"blur"},{min:1,max:64,message:"长度在 1 到 64 个字符",trigger:"blur"}],remark:[{max:100,message:"不能超过 100 个字符",trigger:"blur"}]}}},created:function(){this.loadTable()},methods:{loadTable:function(){this.post("ip.blacklist.page",this.searchFormData,function(a){this.pageInfo=a.data})},onTableUpdate:function(a){var t=this;this.dialogTitle="修改IP",this.dialogVisible=!0,this.$nextTick(function(){Object.assign(t.dialogFormData,a)})},onTableDelete:function(a){this.confirm("确认要移除IP【".concat(a.ip,"】吗?"),function(t){var e={id:a.id};this.post("ip.blacklist.del",e,function(){t(),this.tip("删除成功"),this.loadTable()})})},onDialogSave:function(){var a=this;this.$refs.dialogForm.validate(function(t){if(t){var e=a.dialogFormData.id?"ip.blacklist.update":"ip.blacklist.add";a.post(e,a.dialogFormData,function(){this.dialogVisible=!1,this.loadTable()})}})},onSizeChange:function(a){this.searchFormData.pageSize=a,this.loadTable()},onAdd:function(){this.dialogTitle="新增IP",this.dialogVisible=!0,this.dialogFormData.id=0},onPageIndexChange:function(a){this.searchFormData.pageIndex=a,this.loadTable()}}},r=l,n=e("2877"),s=Object(n["a"])(r,i,o,!1,null,null,null);t["default"]=s.exports}}]); |
@ -0,0 +1 @@ |
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4de1c2b6"],{"26fc":function(t,s,a){t.exports=a.p+"static/img/404_cloud.0f4bc32b.png"},"8cdb":function(t,s,a){"use strict";a.r(s);var e=function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"wscn-http404-container"},[a("div",{staticClass:"wscn-http404"},[t._m(0),t._v(" "),a("div",{staticClass:"bullshit"},[a("div",{staticClass:"bullshit__oops"},[t._v("OOPS!")]),t._v(" "),t._m(1),t._v(" "),a("div",{staticClass:"bullshit__headline"},[t._v(t._s(t.message))]),t._v(" "),a("div",{staticClass:"bullshit__info"},[t._v("Please check that the URL you entered is correct, or click the button below to return to the homepage.")]),t._v(" "),a("a",{staticClass:"bullshit__return-home",attrs:{href:""}},[t._v("Back to home")])])])])},c=[function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"pic-404"},[e("img",{staticClass:"pic-404__parent",attrs:{src:a("a36b"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child left",attrs:{src:a("26fc"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child mid",attrs:{src:a("26fc"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child right",attrs:{src:a("26fc"),alt:"404"}})])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"bullshit__info"},[t._v("All rights reserved\n "),a("a",{staticStyle:{color:"#20a0ff"},attrs:{href:"https://wallstreetcn.com",target:"_blank"}},[t._v("wallstreetcn")])])}],i={name:"Page404",computed:{message:function(){return"The webmaster said that you can not enter this page..."}}},l=i,n=(a("97ef"),a("2877")),r=Object(n["a"])(l,e,c,!1,null,"c095f994",null);s["default"]=r.exports},"97ef":function(t,s,a){"use strict";var e=a("d3f45"),c=a.n(e);c.a},a36b:function(t,s,a){t.exports=a.p+"static/img/404.a57b6f31.png"},d3f45:function(t,s,a){}}]); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-c3ce42fe"],{5040:function(t,n,e){"use strict";var c=e("585b"),s=e.n(c);s.a},"585b":function(t,n,e){},9406:function(t,n,e){"use strict";e.r(n);var c=function(){var t=this,n=t.$createElement;t._self._c;return t._m(0)},s=[function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",{staticClass:"dashboard-container"},[e("div",{staticClass:"dashboard-text"},[t._v("欢迎使用SOP Admin")])])}],a={created:function(){this.post("admin.userinfo.get",{},function(t){})}},i=a,r=(e("5040"),e("2877")),o=Object(r["a"])(i,c,s,!1,null,"b4bd5162",null);n["default"]=o.exports}}]); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@ |
||||
HELP.md |
||||
/target/ |
||||
!.mvn/wrapper/maven-wrapper.jar |
||||
|
||||
### STS ### |
||||
.apt_generated |
||||
.classpath |
||||
.factorypath |
||||
.project |
||||
.settings |
||||
.springBeans |
||||
.sts4-cache |
||||
|
||||
### IntelliJ IDEA ### |
||||
.idea |
||||
*.iws |
||||
*.iml |
||||
*.ipr |
||||
|
||||
### NetBeans ### |
||||
/nbproject/private/ |
||||
/nbbuild/ |
||||
/dist/ |
||||
/nbdist/ |
||||
/.nb-gradle/ |
||||
/build/ |
||||
|
||||
### VS Code ### |
||||
.vscode/ |
@ -0,0 +1,135 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-parent</artifactId> |
||||
<version>2.1.2.RELEASE</version> |
||||
<relativePath/> <!-- lookup parent from repository --> |
||||
</parent> |
||||
<groupId>com.gitee.sop</groupId> |
||||
<artifactId>sop-auth</artifactId> |
||||
<version>1.0.0-SNAPSHOT</version> |
||||
<name>sop-auth</name> |
||||
<description>授权程序</description> |
||||
|
||||
<properties> |
||||
<java.version>1.8</java.version> |
||||
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version> |
||||
<oltu.version>0.31</oltu.version> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
|
||||
<!-- sop相关配置 --> |
||||
<dependency> |
||||
<groupId>com.gitee.sop</groupId> |
||||
<artifactId>sop-service-common</artifactId> |
||||
<version>2.0.0-SNAPSHOT</version> |
||||
</dependency> |
||||
<!-- sop相关配置 end--> |
||||
|
||||
<!-- 使用nacos注册中心 |
||||
版本 0.2.x.RELEASE 对应的是 Spring Boot 2.x 版本,版本 0.1.x.RELEASE 对应的是 Spring Boot 1.x 版本。 |
||||
https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-alibaba-nacos-discovery |
||||
--> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> |
||||
<version>0.2.2.RELEASE</version> |
||||
<exclusions> |
||||
<exclusion> |
||||
<groupId>com.alibaba.nacos</groupId> |
||||
<artifactId>nacos-client</artifactId> |
||||
</exclusion> |
||||
</exclusions> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.alibaba.nacos</groupId> |
||||
<artifactId>nacos-client</artifactId> |
||||
<version>1.1.0</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>com.gitee.sop</groupId> |
||||
<artifactId>sdk-java</artifactId> |
||||
<version>1.0.0-SNAPSHOT</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>net.oschina.durcframework</groupId> |
||||
<artifactId>fastmybatis-spring-boot-starter</artifactId> |
||||
<version>1.7.1</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>mysql</groupId> |
||||
<artifactId>mysql-connector-java</artifactId> |
||||
<scope>runtime</scope> |
||||
</dependency> |
||||
|
||||
<!-- oauth2服务端 --> |
||||
<dependency> |
||||
<groupId>org.apache.oltu.oauth2</groupId> |
||||
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId> |
||||
<version>${oltu.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.oltu.oauth2</groupId> |
||||
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> |
||||
<version>${oltu.version}</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-web</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-data-redis</artifactId> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>org.projectlombok</groupId> |
||||
<artifactId>lombok</artifactId> |
||||
<optional>true</optional> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-test</artifactId> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.google.guava</groupId> |
||||
<artifactId>guava</artifactId> |
||||
<version>27.1-jre</version> |
||||
<scope>compile</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
|
||||
<dependencyManagement> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-dependencies</artifactId> |
||||
<version>${spring-cloud.version}</version> |
||||
<type>pom</type> |
||||
<scope>import</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
</dependencyManagement> |
||||
|
||||
<build> |
||||
<plugins> |
||||
<plugin> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-maven-plugin</artifactId> |
||||
</plugin> |
||||
</plugins> |
||||
</build> |
||||
|
||||
</project> |
@ -0,0 +1,11 @@ |
||||
# 应用授权服务 |
||||
|
||||
- 启动注册中心、网关、本服务(sop-auth) |
||||
- 浏览器访问:http://localhost:8087/oauth2/appToAppAuth?app_id=2019032617262200001&redirect_uri=http%3a%2f%2flocalhost%3a8087%2foauth2callback |
||||
- 输入用户名密码登录,这里是`zhangsan/123456` |
||||
|
||||
授权接口在`OAuth2Controller`中,查看回调接口在`CallbackController`中 |
||||
|
||||
回调接口应该由开发者实现,这里为了演示,写在一起。 |
||||
|
||||
token的维护,重点关注`OAuth2ManagerRedis.java` |
@ -0,0 +1,15 @@ |
||||
package com.gitee.sop.sopauth; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; |
||||
|
||||
@EnableDiscoveryClient |
||||
@SpringBootApplication |
||||
public class SopAuthApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(SopAuthApplication.class, args); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,44 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class AccessToken { |
||||
/** 过期时间,毫秒 */ |
||||
private long expireTimeMillis; |
||||
private OpenUser openUser; |
||||
|
||||
/** |
||||
* @param expireIn 有效时长,秒 |
||||
* @param openUser |
||||
*/ |
||||
public AccessToken(long expireIn, OpenUser openUser) { |
||||
super(); |
||||
if(expireIn <= 0) { |
||||
throw new IllegalArgumentException("expireIn必须大于0"); |
||||
} |
||||
this.expireTimeMillis = System.currentTimeMillis() + (expireIn * 1000); |
||||
this.openUser = openUser; |
||||
} |
||||
|
||||
public boolean isExpired() { |
||||
return expireTimeMillis < System.currentTimeMillis(); |
||||
} |
||||
|
||||
public long getExpireTimeMillis() { |
||||
return expireTimeMillis; |
||||
} |
||||
|
||||
public void setExpireTimeMillis(long expireTimeMillis) { |
||||
this.expireTimeMillis = expireTimeMillis; |
||||
} |
||||
|
||||
public OpenUser getOpenUser() { |
||||
return openUser; |
||||
} |
||||
|
||||
public void setOpenUser(OpenUser openUser) { |
||||
this.openUser = openUser; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,14 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface AppIdManager { |
||||
/** |
||||
* 是否是合法的appId |
||||
* |
||||
* @param appId |
||||
* @return true:合法 |
||||
*/ |
||||
boolean isValidAppId(String appId); |
||||
} |
@ -0,0 +1,30 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import javax.validation.constraints.NotBlank; |
||||
|
||||
/** |
||||
* 获取accessToken参数对象 |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class FetchTokenParam { |
||||
/** |
||||
* 授权类型。<br> |
||||
* 如果使用app_auth_code换取token,则为authorization_code, |
||||
* 如果使用refresh_token换取新的token,则为refresh_token |
||||
*/ |
||||
@NotBlank(message = "授权类型不能为空") |
||||
private String grant_type; |
||||
|
||||
/** |
||||
* 授权码.与refresh_token二选一,用户对应用授权后得到,即第一步中开发者获取到的app_auth_code值 |
||||
*/ |
||||
private String code; |
||||
|
||||
/** |
||||
* 刷新令牌.与code二选一,可为空,刷新令牌时使用 |
||||
*/ |
||||
private String refresh_token; |
||||
} |
@ -0,0 +1,39 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* 使用app_auth_code换取app_auth_token返回结果 |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class FetchTokenResult { |
||||
/** |
||||
* 授权令牌 |
||||
*/ |
||||
private String app_auth_token; |
||||
|
||||
/** |
||||
* 用户id |
||||
*/ |
||||
private String user_id; |
||||
|
||||
/** |
||||
* 令牌有效期. -1:永久有效 |
||||
*/ |
||||
private long expires_in = -1; |
||||
|
||||
/** |
||||
* 刷新令牌有效期. -1:永久有效 |
||||
*/ |
||||
private long re_expires_in = -1; |
||||
|
||||
/** |
||||
* 刷新令牌时使用。 |
||||
* 刷新令牌后,我们会保证老的app_auth_token从刷新开始10分钟内可继续使用,请及时替换为最新token |
||||
*/ |
||||
private String app_refresh_token; |
||||
|
||||
private String error; |
||||
private String error_description; |
||||
} |
@ -0,0 +1,46 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class OAuth2Config { |
||||
|
||||
private static OAuth2Config instance = new OAuth2Config(); |
||||
|
||||
public static OAuth2Config getInstance() { |
||||
return instance; |
||||
} |
||||
|
||||
public static void setInstance(OAuth2Config instance) { |
||||
OAuth2Config.instance = instance; |
||||
} |
||||
|
||||
/** |
||||
* 应用授权的app_auth_code唯一的;app_auth_code使用一次后失效,一天(从生成app_auth_code开始的24小时)未被使用自动过期; |
||||
* app_auth_token永久有效。 |
||||
*/ |
||||
private int codeTimeoutSeconds = 86400; |
||||
/** |
||||
* accessToken有效时间,单位秒 |
||||
* -1 永久有效 |
||||
*/ |
||||
private int accessTokenExpiresIn = -1; |
||||
|
||||
/** |
||||
* app_refresh_token有效时间,单位秒,accessToken有效时间的3倍。当小于0,永久有效 |
||||
*/ |
||||
private int refreshTokenExpiresIn = accessTokenExpiresIn * 3; |
||||
|
||||
/** |
||||
* 刷新令牌后,我们会保证老的app_auth_token从刷新开始10分钟内可继续使用,请及时替换为最新token |
||||
*/ |
||||
private int afterRefreshExpiresIn = 60 * 10; |
||||
|
||||
/** |
||||
* 登录视图页面用于,mvc视图,如:loginView |
||||
*/ |
||||
private String oauth2LoginUri = "/oauth2/login"; |
||||
} |
@ -0,0 +1,85 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
|
||||
import com.gitee.sop.sopauth.auth.exception.LoginErrorException; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
|
||||
/** |
||||
* 认证服务,需要自己实现 |
||||
* @author tanghc |
||||
* |
||||
*/ |
||||
public interface OAuth2Manager { |
||||
|
||||
/** |
||||
* 添加 auth code |
||||
* |
||||
* @param authCode |
||||
* code值 |
||||
* @param authUser |
||||
* 用户 |
||||
*/ |
||||
void addAuthCode(String authCode, OpenUser authUser); |
||||
|
||||
/** |
||||
* 添加accessToken |
||||
* @param accessToken token值 |
||||
* @param refreshToken refreshToken |
||||
* @param authUser 用户 |
||||
*/ |
||||
void addAccessToken(String accessToken, String refreshToken, OpenUser authUser); |
||||
|
||||
|
||||
/** |
||||
* 删除这个accessToken |
||||
* @param accessToken |
||||
*/ |
||||
void removeAccessToken(String accessToken); |
||||
|
||||
/** |
||||
* 删除这个refreshToken |
||||
* @param refreshToken |
||||
*/ |
||||
void removeRefreshToken(String refreshToken); |
||||
|
||||
/** |
||||
* 获取RefreshToken |
||||
* @param refreshToken |
||||
* @return 返回Token信息 |
||||
*/ |
||||
RefreshToken getRefreshToken(String refreshToken); |
||||
|
||||
/** |
||||
* 验证auth code是否有效 |
||||
* |
||||
* @param authCode |
||||
* @return 无效返回false |
||||
*/ |
||||
boolean checkAuthCode(String authCode); |
||||
|
||||
/** |
||||
* 根据auth code获取用户 |
||||
* |
||||
* @param authCode |
||||
* @return 返回用户 |
||||
*/ |
||||
OpenUser getUserByAuthCode(String authCode); |
||||
|
||||
/** |
||||
* 根据access token获取用户 |
||||
* |
||||
* @param accessToken |
||||
* token值 |
||||
* @return 返回用户 |
||||
*/ |
||||
OpenUser getUserByAccessToken(String accessToken); |
||||
|
||||
/** |
||||
* 用户登录,需判断是否已经登录 |
||||
* @param request |
||||
* @return 返回用户对象 |
||||
* @throws LoginErrorException 登录失败异常 |
||||
*/ |
||||
OpenUser login(HttpServletRequest request) throws LoginErrorException; |
||||
} |
@ -0,0 +1,44 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import org.apache.oltu.oauth2.common.exception.OAuthSystemException; |
||||
import org.apache.oltu.oauth2.common.message.OAuthResponse; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
import java.net.URISyntaxException; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface OAuth2Service { |
||||
|
||||
/** |
||||
* oauth2授权,获取code. |
||||
* <pre> |
||||
* 1、首先通过如http://localhost:8080/api/authorize?client_id=test&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2callback访问授权页面;
|
||||
2、该控制器首先检查clientId是否正确;如果错误将返回相应的错误信息; |
||||
3、然后判断用户是否登录了,如果没有登录首先到登录页面登录; |
||||
4、登录成功后生成相应的code即授权码,然后重定向到客户端地址,如http://localhost:8080/oauth2callback?code=6d250650831fea227749f49a5b49ccad;在重定向到的地址中会带上code参数(授权码),接着客户端可以根据授权码去换取accessToken。
|
||||
* </pre> |
||||
* |
||||
* @param request req |
||||
* @param response resp |
||||
* @param authConfig 配置 |
||||
* @return 返回响应内容 |
||||
* @throws URISyntaxException |
||||
* @throws OAuthSystemException |
||||
*/ |
||||
OAuthResponse authorize(HttpServletRequest request, HttpServletResponse response, OAuth2Config authConfig) |
||||
throws URISyntaxException, OAuthSystemException; |
||||
|
||||
|
||||
/** |
||||
* 通过code获取accessToken. |
||||
* @param fetchTokenParam |
||||
* @param authConfig 配置项 |
||||
* @return 返回响应内容 |
||||
* @throws URISyntaxException |
||||
* @throws OAuthSystemException |
||||
*/ |
||||
FetchTokenResult accessToken(FetchTokenParam fetchTokenParam, OAuth2Config authConfig); |
||||
} |
@ -0,0 +1,22 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* 如果要使用oauth2功能,自己的用户类需要实现这个对象 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
public interface OpenUser extends Serializable { |
||||
/** |
||||
* 返回用户id |
||||
* @return |
||||
*/ |
||||
String getUserId(); |
||||
/** |
||||
* 返回用户名 |
||||
* |
||||
* @return 返回用户名 |
||||
*/ |
||||
String getUsername(); |
||||
} |
@ -0,0 +1,40 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class RefreshToken implements Serializable { |
||||
private static final long serialVersionUID = 1L; |
||||
|
||||
private String accessToken; |
||||
private OpenUser openUser; |
||||
|
||||
public RefreshToken(String accessToken, OpenUser openUser) { |
||||
super(); |
||||
this.accessToken = accessToken; |
||||
this.openUser = openUser; |
||||
} |
||||
|
||||
public RefreshToken() { |
||||
super(); |
||||
} |
||||
|
||||
public String getAccessToken() { |
||||
return accessToken; |
||||
} |
||||
|
||||
public void setAccessToken(String accessToken) { |
||||
this.accessToken = accessToken; |
||||
} |
||||
|
||||
public OpenUser getOpenUser() { |
||||
return openUser; |
||||
} |
||||
|
||||
public void setOpenUser(OpenUser openUser) { |
||||
this.openUser = openUser; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,29 @@ |
||||
package com.gitee.sop.sopauth.auth; |
||||
|
||||
/** |
||||
* 存放accessToken和refreshToken |
||||
* |
||||
* @author tanghc |
||||
* |
||||
*/ |
||||
public class TokenPair { |
||||
private String accessToken; |
||||
private String refreshToken; |
||||
|
||||
public String getAccessToken() { |
||||
return accessToken; |
||||
} |
||||
|
||||
public void setAccessToken(String accessToken) { |
||||
this.accessToken = accessToken; |
||||
} |
||||
|
||||
public String getRefreshToken() { |
||||
return refreshToken; |
||||
} |
||||
|
||||
public void setRefreshToken(String refreshToken) { |
||||
this.refreshToken = refreshToken; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,25 @@ |
||||
package com.gitee.sop.sopauth.auth.exception; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class LoginErrorException extends Exception { |
||||
private static final long serialVersionUID = -6721499454527023339L; |
||||
|
||||
public LoginErrorException() { |
||||
super(); |
||||
} |
||||
|
||||
public LoginErrorException(String message, Throwable cause) { |
||||
super(message, cause); |
||||
} |
||||
|
||||
public LoginErrorException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public LoginErrorException(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,33 @@ |
||||
package com.gitee.sop.sopauth.auth.impl; |
||||
|
||||
import com.gitee.sop.sopauth.auth.AppIdManager; |
||||
import com.gitee.sop.sopauth.entity.IsvInfo; |
||||
import com.gitee.sop.sopauth.mapper.IsvInfoMapper; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Service |
||||
@Slf4j |
||||
public class AppIdManagerImpl implements AppIdManager { |
||||
|
||||
public static final int FORBIDDEN = 2; |
||||
@Autowired |
||||
private IsvInfoMapper isvInfoMapper; |
||||
|
||||
@Override |
||||
public boolean isValidAppId(String appId) { |
||||
IsvInfo isvInfo = isvInfoMapper.getByColumn("app_key", appId); |
||||
if (isvInfo == null) { |
||||
return false; |
||||
} |
||||
if (isvInfo.getStatus().intValue() == FORBIDDEN) { |
||||
log.error("appId已禁用:{}", appId); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,128 @@ |
||||
package com.gitee.sop.sopauth.auth.impl; |
||||
|
||||
import com.gitee.fastmybatis.core.query.Query; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Config; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Manager; |
||||
import com.gitee.sop.sopauth.auth.OpenUser; |
||||
import com.gitee.sop.sopauth.auth.RefreshToken; |
||||
import com.gitee.sop.sopauth.auth.exception.LoginErrorException; |
||||
import com.gitee.sop.sopauth.entity.UserInfo; |
||||
import com.gitee.sop.sopauth.mapper.UserInfoMapper; |
||||
import com.google.common.cache.CacheBuilder; |
||||
import com.google.common.cache.CacheLoader; |
||||
import com.google.common.cache.LoadingCache; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* oauth2管理,默认谷歌缓存实现,跟redis实现只能用一个。 |
||||
* 这里为了演示,使用本地缓存,正式环境请使用redis保存 |
||||
* @see OAuth2ManagerRedis OAuth2ManagerRedis |
||||
* @author tanghc |
||||
* |
||||
*/ |
||||
@Service |
||||
public class OAuth2ManagerCache implements OAuth2Manager { |
||||
/** |
||||
* 应用授权的app_auth_code唯一的;app_auth_code使用一次后失效,一天(从生成app_auth_code开始的24小时)未被使用自动过期; |
||||
* app_auth_token永久有效。 |
||||
*/ |
||||
private int codeTimeoutSeconds = OAuth2Config.getInstance().getCodeTimeoutSeconds(); |
||||
/** |
||||
* accessToken过期时间 |
||||
* -1 永久有效 |
||||
*/ |
||||
private int accessTokenTimeoutSeconds = OAuth2Config.getInstance().getAccessTokenExpiresIn(); |
||||
|
||||
private int refreshTokenTimeoutSeconds = OAuth2Config.getInstance().getRefreshTokenExpiresIn(); |
||||
|
||||
private LoadingCache<String, OpenUser> codeCache = buildCache(codeTimeoutSeconds); |
||||
private LoadingCache<String, OpenUser> accessTokenCache = buildCache(accessTokenTimeoutSeconds); |
||||
private LoadingCache<String, RefreshToken> refreshTokenCache = buildCache(refreshTokenTimeoutSeconds); |
||||
|
||||
@Autowired |
||||
private UserInfoMapper userInfoMapper; |
||||
|
||||
|
||||
private static <T> LoadingCache<String, T> buildCache(int timeout) { |
||||
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder(); |
||||
if (timeout > 0) { |
||||
cacheBuilder.expireAfterAccess(timeout, TimeUnit.SECONDS); |
||||
} |
||||
return cacheBuilder |
||||
.build(new CacheLoader<String, T>() { |
||||
@Override |
||||
public T load(String key) throws Exception { |
||||
return null; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
public void addAuthCode(String authCode, OpenUser authUser) { |
||||
codeCache.put(authCode, authUser); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void addAccessToken(String accessToken, String refreshToken, OpenUser authUser) { |
||||
accessTokenCache.put(accessToken, authUser); |
||||
refreshTokenCache.put(refreshToken, new RefreshToken(accessToken, authUser)); |
||||
} |
||||
|
||||
@Override |
||||
public void removeAccessToken(String accessToken) { |
||||
accessTokenCache.asMap().remove(accessToken); |
||||
} |
||||
|
||||
@Override |
||||
public void removeRefreshToken(String refreshToken) { |
||||
refreshTokenCache.asMap().remove(refreshToken); |
||||
} |
||||
|
||||
@Override |
||||
public RefreshToken getRefreshToken(String refreshToken) { |
||||
return refreshTokenCache.getIfPresent(refreshToken); |
||||
} |
||||
|
||||
@Override |
||||
public boolean checkAuthCode(String authCode) { |
||||
return codeCache.asMap().containsKey(authCode); |
||||
} |
||||
|
||||
@Override |
||||
public OpenUser getUserByAuthCode(String authCode) { |
||||
return codeCache.getIfPresent(authCode); |
||||
} |
||||
|
||||
@Override |
||||
public OpenUser getUserByAccessToken(String accessToken) { |
||||
return accessTokenCache.getIfPresent(accessToken); |
||||
} |
||||
|
||||
@Override |
||||
public OpenUser login(HttpServletRequest request) throws LoginErrorException { |
||||
// 这里应该先检查用户有没有登录,如果登录直接返回openUser
|
||||
String username = request.getParameter("username"); |
||||
String password = request.getParameter("password"); |
||||
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { |
||||
throw new LoginErrorException("用户名密码不能为空"); |
||||
} |
||||
|
||||
Query query = new Query(); |
||||
query.eq("username", username) |
||||
.eq("password", password); |
||||
UserInfo userInfo = userInfoMapper.getByQuery(query); |
||||
|
||||
if(userInfo == null) { |
||||
throw new LoginErrorException("用户名密码不正确"); |
||||
} |
||||
|
||||
return userInfo; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,168 @@ |
||||
package com.gitee.sop.sopauth.auth.impl; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
import com.gitee.fastmybatis.core.query.Query; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Config; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Manager; |
||||
import com.gitee.sop.sopauth.auth.OpenUser; |
||||
import com.gitee.sop.sopauth.auth.RefreshToken; |
||||
import com.gitee.sop.sopauth.auth.exception.LoginErrorException; |
||||
import com.gitee.sop.sopauth.entity.UserInfo; |
||||
import com.gitee.sop.sopauth.mapper.UserInfoMapper; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* |
||||
* oauth2管理redis实现,这个类跟OAuth2ManagerCache类只能用一个, |
||||
* 如果要用这个类, |
||||
* 1、注释掉OAuth2ManagerCache的@Service。 |
||||
* 2、打开yml中redis部分 |
||||
* 3、启用这个类的@Service |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
//@Service
|
||||
public class OAuth2ManagerRedis implements OAuth2Manager { |
||||
|
||||
private static String CODE_PREFIX = "com.gitee.sop.oauth2_code:"; |
||||
private static String ACCESS_TOKEN_PREFIX = "com.gitee.sop.oauth2_access_token:"; |
||||
private static String REFRESH_TOKEN_PREFIX = "com.gitee.sop.oauth2_refresh_token:"; |
||||
|
||||
@Autowired |
||||
private StringRedisTemplate redisTemplate; |
||||
|
||||
@Autowired |
||||
private UserInfoMapper userInfoMapper; |
||||
|
||||
public static String getCodeKey(String code) { |
||||
return CODE_PREFIX + code; |
||||
} |
||||
|
||||
public static String getAccessTokenKey(String accessToken) { |
||||
return ACCESS_TOKEN_PREFIX + accessToken; |
||||
} |
||||
|
||||
public static String getRefreshTokenKey(String refreshToken) { |
||||
return REFRESH_TOKEN_PREFIX + refreshToken; |
||||
} |
||||
|
||||
@Override |
||||
public void addAuthCode(String authCode, OpenUser authUser) { |
||||
long codeTimeoutSeconds = OAuth2Config.getInstance().getCodeTimeoutSeconds(); |
||||
redisTemplate.opsForValue().set(getCodeKey(authCode), |
||||
JSON.toJSONString(authUser), |
||||
codeTimeoutSeconds, |
||||
TimeUnit.SECONDS); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void addAccessToken(String accessToken, String refreshToken, OpenUser authUser) { |
||||
// 存accessToken
|
||||
long expiresIn = OAuth2Config.getInstance().getAccessTokenExpiresIn(); |
||||
long reExpiresIn = OAuth2Config.getInstance().getRefreshTokenExpiresIn(); |
||||
if (expiresIn > 0) { |
||||
redisTemplate.opsForValue().set(getAccessTokenKey(accessToken), JSON.toJSONString(authUser), expiresIn, TimeUnit.SECONDS); |
||||
} else { |
||||
redisTemplate.opsForValue().set(getAccessTokenKey(accessToken), JSON.toJSONString(authUser)); |
||||
} |
||||
// 存refreshToken
|
||||
if (reExpiresIn > 0) { |
||||
redisTemplate.opsForValue().set( |
||||
getRefreshTokenKey(refreshToken), |
||||
JSON.toJSONString(new RefreshToken(accessToken, authUser)), |
||||
// refreshToken过期时间
|
||||
reExpiresIn, |
||||
TimeUnit.SECONDS); |
||||
} else { |
||||
redisTemplate.opsForValue().set( |
||||
getRefreshTokenKey(refreshToken), |
||||
JSON.toJSONString(new RefreshToken(accessToken, authUser))); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void removeAccessToken(String accessToken) { |
||||
String accessTokenKey = getAccessTokenKey(accessToken); |
||||
int afterRefreshExpiresIn = OAuth2Config.getInstance().getAfterRefreshExpiresIn(); |
||||
// 刷新令牌后,保证老的app_auth_token从刷新开始10分钟内可继续使用
|
||||
redisTemplate.expire(accessTokenKey, afterRefreshExpiresIn, TimeUnit.SECONDS); |
||||
} |
||||
|
||||
@Override |
||||
public void removeRefreshToken(String refreshToken) { |
||||
redisTemplate.delete(getRefreshTokenKey(refreshToken)); |
||||
} |
||||
|
||||
@Override |
||||
public RefreshToken getRefreshToken(String refreshToken) { |
||||
String json = redisTemplate.opsForValue().get(getRefreshTokenKey(refreshToken)); |
||||
if(StringUtils.isEmpty(json)) { |
||||
return null; |
||||
} |
||||
JSONObject jsonObj = JSON.parseObject(json); |
||||
|
||||
String userJson = jsonObj.getString("openUser"); |
||||
UserInfo user = JSON.parseObject(userJson, UserInfo.class); |
||||
String accessToken = jsonObj.getString("accessToken"); |
||||
|
||||
return new RefreshToken(accessToken, user); |
||||
} |
||||
|
||||
@Override |
||||
public boolean checkAuthCode(String authCode) { |
||||
if(StringUtils.isEmpty(authCode)) { |
||||
return false; |
||||
} |
||||
return redisTemplate.hasKey(getCodeKey(authCode)); |
||||
} |
||||
|
||||
@Override |
||||
public OpenUser getUserByAuthCode(String authCode) { |
||||
String json = redisTemplate.opsForValue().get(getCodeKey(authCode)); |
||||
if(StringUtils.isEmpty(json)) { |
||||
return null; |
||||
} |
||||
return JSON.parseObject(json, UserInfo.class); |
||||
} |
||||
|
||||
@Override |
||||
public OpenUser getUserByAccessToken(String accessToken) { |
||||
String json = redisTemplate.opsForValue().get(getAccessTokenKey(accessToken)); |
||||
if(StringUtils.isEmpty(json)) { |
||||
return null; |
||||
} |
||||
return JSON.parseObject(json, UserInfo.class); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public OpenUser login(HttpServletRequest request) throws LoginErrorException { |
||||
// 这里应该先检查用户有没有登录,如果登录直接返回openUser
|
||||
|
||||
String username = request.getParameter("username"); |
||||
String password = request.getParameter("password"); |
||||
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { |
||||
throw new LoginErrorException("用户名密码不能为空"); |
||||
} |
||||
|
||||
Query query = new Query(); |
||||
query.eq("username", username) |
||||
.eq("password", password); |
||||
UserInfo userInfo = userInfoMapper.getByQuery(query); |
||||
|
||||
if(userInfo == null) { |
||||
throw new LoginErrorException("用户名密码不正确"); |
||||
} |
||||
|
||||
return userInfo; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,262 @@ |
||||
package com.gitee.sop.sopauth.auth.impl; |
||||
|
||||
import com.gitee.sop.sopauth.auth.AppIdManager; |
||||
import com.gitee.sop.sopauth.auth.FetchTokenParam; |
||||
import com.gitee.sop.sopauth.auth.FetchTokenResult; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Config; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Manager; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Service; |
||||
import com.gitee.sop.sopauth.auth.OpenUser; |
||||
import com.gitee.sop.sopauth.auth.RefreshToken; |
||||
import com.gitee.sop.sopauth.auth.TokenPair; |
||||
import com.gitee.sop.sopauth.auth.exception.LoginErrorException; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.oltu.oauth2.as.issuer.MD5Generator; |
||||
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; |
||||
import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; |
||||
import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; |
||||
import org.apache.oltu.oauth2.as.response.OAuthASResponse; |
||||
import org.apache.oltu.oauth2.common.OAuth; |
||||
import org.apache.oltu.oauth2.common.error.OAuthError; |
||||
import org.apache.oltu.oauth2.common.exception.OAuthProblemException; |
||||
import org.apache.oltu.oauth2.common.exception.OAuthSystemException; |
||||
import org.apache.oltu.oauth2.common.message.OAuthResponse; |
||||
import org.apache.oltu.oauth2.common.message.types.GrantType; |
||||
import org.apache.oltu.oauth2.common.message.types.ResponseType; |
||||
import org.apache.oltu.oauth2.common.utils.OAuthUtils; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
import java.net.URISyntaxException; |
||||
|
||||
/** |
||||
* oauth2服务端默认实现 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Service |
||||
@Slf4j |
||||
public class OAuth2ServiceImpl implements OAuth2Service { |
||||
|
||||
|
||||
private static final String TOKEN_TYPE = "Bearer"; |
||||
public static final String APP_ID_NAME = "app_id"; |
||||
|
||||
private OAuthIssuer oauthIssuer = new OAuthIssuerImpl(new MD5Generator()); |
||||
|
||||
@Autowired |
||||
private OAuth2Manager oauth2Manager; |
||||
|
||||
@Autowired |
||||
private AppIdManager appIdManager; |
||||
|
||||
@Override |
||||
public OAuthResponse authorize(HttpServletRequest request, HttpServletResponse resp, OAuth2Config authConfig) |
||||
throws URISyntaxException, OAuthSystemException { |
||||
try { |
||||
// 构建OAuth 授权请求
|
||||
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request); |
||||
String clientId = oauthRequest.getClientId(); |
||||
// 检查传入的客户端id是否正确
|
||||
if (!appIdManager.isValidAppId(clientId)) { |
||||
return OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST) |
||||
.setError(OAuthError.TokenResponse.INVALID_CLIENT) |
||||
.setErrorDescription(OAuthError.TokenResponse.INVALID_CLIENT).buildJSONMessage(); |
||||
} |
||||
|
||||
// 如果用户没有登录,跳转到登陆页面
|
||||
OpenUser user = null; |
||||
try { |
||||
user = oauth2Manager.login(request); |
||||
} catch (LoginErrorException e) { |
||||
log.error(e.getMessage(), e); |
||||
request.setAttribute("error", e.getMessage()); |
||||
try { |
||||
request.getRequestDispatcher(authConfig.getOauth2LoginUri()).forward(request, resp); |
||||
return null; |
||||
} catch (Exception e1) { |
||||
log.error(e1.getMessage(), e1); |
||||
throw new RuntimeException(e1); |
||||
} |
||||
} |
||||
|
||||
// 生成授权码
|
||||
String authorizationCode = null; |
||||
// responseType目前仅支持CODE,另外还有TOKEN
|
||||
String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE); |
||||
if (responseType.equals(ResponseType.CODE.toString())) { |
||||
OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator()); |
||||
authorizationCode = oauthIssuerImpl.authorizationCode(); |
||||
oauth2Manager.addAuthCode(authorizationCode, user); |
||||
} |
||||
// 进行OAuth响应构建
|
||||
OAuthASResponse.OAuthAuthorizationResponseBuilder builder = OAuthASResponse.authorizationResponse(request, |
||||
HttpServletResponse.SC_FOUND); |
||||
// 设置授权码
|
||||
builder.setCode(authorizationCode); |
||||
builder.setParam(APP_ID_NAME, clientId); |
||||
// 得到到客户端重定向地址
|
||||
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI); |
||||
|
||||
// 构建响应
|
||||
return builder.location(redirectURI).buildQueryMessage(); |
||||
} catch (OAuthProblemException e) { |
||||
// 出错处理
|
||||
String redirectUri = e.getRedirectUri(); |
||||
if (OAuthUtils.isEmpty(redirectUri)) { |
||||
// 告诉客户端没有传入redirectUri直接报错
|
||||
String error = "OAuth redirectUri needs to be provided by client!!!"; |
||||
return OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND) |
||||
.error(OAuthProblemException.error(error)).location(redirectUri).buildQueryMessage(); |
||||
} else { |
||||
// 返回错误消息(如?error=)
|
||||
return OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND).error(e).location(redirectUri) |
||||
.buildQueryMessage(); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public FetchTokenResult accessToken(FetchTokenParam fetchTokenParam, OAuth2Config authConfig) { |
||||
try { |
||||
// 这里需要检查client_id和client_secret,但是已经通过网关接口访问进来了
|
||||
// 表示client_id和client_secret是合法的
|
||||
|
||||
// 检查验证类型,如果是第一次进来用code换取accessToken
|
||||
String grant_type = fetchTokenParam.getGrant_type(); |
||||
if (GrantType.AUTHORIZATION_CODE.toString().equals(grant_type)) { |
||||
String authCode = fetchTokenParam.getCode(); |
||||
if (!oauth2Manager.checkAuthCode(authCode)) { |
||||
FetchTokenResult errorResult = new FetchTokenResult(); |
||||
errorResult.setError(OAuthError.CodeResponse.INVALID_REQUEST); |
||||
errorResult.setError_description("invalid request"); |
||||
return errorResult; |
||||
} |
||||
// 生成Access Token
|
||||
OpenUser user = oauth2Manager.getUserByAuthCode(authCode); |
||||
if (user == null) { |
||||
throw OAuthProblemException.error("Can not found user by code."); |
||||
} |
||||
|
||||
TokenPair tokenPair = this.createNewToken(user); |
||||
|
||||
oauth2Manager.addAccessToken(tokenPair.getAccessToken(), tokenPair.getRefreshToken(), user); |
||||
|
||||
// 生成OAuth响应
|
||||
return this.buildTokenResult(tokenPair, user); |
||||
} else if (GrantType.REFRESH_TOKEN.toString().equals(grant_type)) { |
||||
// 用refreshToken来刷新accessToken
|
||||
String refreshToken = fetchTokenParam.getRefresh_token(); |
||||
if (StringUtils.isEmpty(refreshToken)) { |
||||
FetchTokenResult errorResult = new FetchTokenResult(); |
||||
errorResult.setError(OAuthError.ResourceResponse.EXPIRED_TOKEN); |
||||
errorResult.setError_description("expired token"); |
||||
return errorResult; |
||||
} |
||||
|
||||
RefreshToken refreshTokenObj = oauth2Manager.getRefreshToken(refreshToken); |
||||
if (refreshTokenObj == null) { |
||||
FetchTokenResult errorResult = new FetchTokenResult(); |
||||
errorResult.setError(OAuthError.ResourceResponse.INVALID_TOKEN); |
||||
errorResult.setError_description("invalid token"); |
||||
return errorResult; |
||||
} |
||||
|
||||
OpenUser user = refreshTokenObj.getOpenUser(); |
||||
// 老的token对
|
||||
TokenPair oldTokenPair = new TokenPair(); |
||||
oldTokenPair.setAccessToken(refreshTokenObj.getAccessToken()); |
||||
oldTokenPair.setRefreshToken(refreshToken); |
||||
// 创建一对新的accessToken和refreshToken
|
||||
TokenPair newTokenPair = this.createRefreshToken(user, oldTokenPair); |
||||
|
||||
this.afterRefreshToken(oldTokenPair, newTokenPair, user); |
||||
|
||||
// 返回新的accessToken和refreshToken
|
||||
return this.buildTokenResult(newTokenPair, user); |
||||
} else { |
||||
FetchTokenResult errorResult = new FetchTokenResult(); |
||||
errorResult.setError(OAuthError.TokenResponse.INVALID_GRANT); |
||||
errorResult.setError_description("invalid grant"); |
||||
return errorResult; |
||||
} |
||||
|
||||
} catch (OAuthProblemException e) { |
||||
log.error(e.getMessage(), e); |
||||
FetchTokenResult errorResult = new FetchTokenResult(); |
||||
errorResult.setError(e.getMessage()); |
||||
errorResult.setError_description("invalid grant"); |
||||
return errorResult; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 刷新token后续操作 |
||||
* |
||||
* @param oldTokenPair 老的token |
||||
* @param newTokenPair 新的token |
||||
* @param user 用户 |
||||
*/ |
||||
protected void afterRefreshToken(TokenPair oldTokenPair, TokenPair newTokenPair, OpenUser user) { |
||||
// 保存token
|
||||
oauth2Manager.addAccessToken(newTokenPair.getAccessToken(), newTokenPair.getRefreshToken(), user); |
||||
|
||||
// 删除老的accessToken
|
||||
oauth2Manager.removeAccessToken(oldTokenPair.getAccessToken()); |
||||
// 删除老的refreshToken
|
||||
oauth2Manager.removeRefreshToken(oldTokenPair.getRefreshToken()); |
||||
} |
||||
|
||||
/** |
||||
* 创建新的token |
||||
* |
||||
* @param user |
||||
* @return 返回新token |
||||
*/ |
||||
protected TokenPair createNewToken(OpenUser user) { |
||||
return this.createDefaultTokenPair(); |
||||
} |
||||
|
||||
/** |
||||
* 返回刷新后token |
||||
* |
||||
* @param user 用户 |
||||
* @param oldTokenPair 旧的token |
||||
* @return 返回新的token |
||||
*/ |
||||
protected TokenPair createRefreshToken(OpenUser user, TokenPair oldTokenPair) { |
||||
return this.createDefaultTokenPair(); |
||||
} |
||||
|
||||
private TokenPair createDefaultTokenPair() { |
||||
TokenPair tokenPair = new TokenPair(); |
||||
try { |
||||
String accessToken = oauthIssuer.accessToken(); |
||||
String refreshToken = oauthIssuer.refreshToken(); |
||||
|
||||
tokenPair.setAccessToken(accessToken); |
||||
tokenPair.setRefreshToken(refreshToken); |
||||
|
||||
return tokenPair; |
||||
} catch (OAuthSystemException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
private FetchTokenResult buildTokenResult(TokenPair tokenPair, OpenUser user) { |
||||
OAuth2Config auth2Config = OAuth2Config.getInstance(); |
||||
FetchTokenResult fetchTokenResult = new FetchTokenResult(); |
||||
fetchTokenResult.setApp_auth_token(tokenPair.getAccessToken()); |
||||
fetchTokenResult.setApp_refresh_token(tokenPair.getRefreshToken()); |
||||
fetchTokenResult.setUser_id(user.getUserId()); |
||||
fetchTokenResult.setExpires_in(auth2Config.getAccessTokenExpiresIn()); |
||||
fetchTokenResult.setRe_expires_in(auth2Config.getRefreshTokenExpiresIn()); |
||||
return fetchTokenResult; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,20 @@ |
||||
package com.gitee.sop.sopauth.config; |
||||
|
||||
import com.gitee.sop.servercommon.bean.ServiceConfig; |
||||
import com.gitee.sop.servercommon.configuration.AlipayServiceConfiguration; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
/** |
||||
* 使用支付宝开放平台功能 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Configuration |
||||
public class OpenServiceConfig extends AlipayServiceConfiguration { |
||||
|
||||
|
||||
static { |
||||
ServiceConfig.getInstance().getI18nModules().add("i18n/isp/goods_error"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,54 @@ |
||||
package com.gitee.sop.sopauth.controller; |
||||
|
||||
import com.gitee.sop.sdk.client.OpenClient; |
||||
import com.gitee.sop.sdk.model.OpenAuthTokenAppModel; |
||||
import com.gitee.sop.sdk.request.OpenAuthTokenAppRequest; |
||||
import com.gitee.sop.sdk.response.OpenAuthTokenAppResponse; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Controller |
||||
@Slf4j |
||||
public class CallbackController { |
||||
String url = "http://localhost:8081"; |
||||
String appId = "2019032617262200001"; |
||||
// 平台提供的私钥
|
||||
String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ="; |
||||
|
||||
OpenClient openClient = new OpenClient(url, appId, privateKey); |
||||
|
||||
/** |
||||
* 模拟开发者回调,这里需要开发者自己实现.通过code换取token |
||||
* @return |
||||
*/ |
||||
@GetMapping("oauth2callback") |
||||
@ResponseBody |
||||
public OpenAuthTokenAppResponse callback(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { |
||||
String app_id = servletRequest.getParameter("app_id"); |
||||
String code = servletRequest.getParameter("code"); |
||||
log.info("app_id:{}, code:{}", app_id, code); |
||||
|
||||
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest(); |
||||
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel(); |
||||
model.setCode(code); |
||||
model.setGrant_type("authorization_code"); |
||||
request.setBizModel(model); |
||||
|
||||
// 根据code获取token
|
||||
OpenAuthTokenAppResponse response = openClient.execute(request); |
||||
if (response.isSuccess()) { |
||||
// 成功拿到token,开发者在这里保存token信息
|
||||
// 后续使用token进行接口访问
|
||||
log.info("授权成功,body:{}", response.getBody()); |
||||
} |
||||
return response; |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
package com.gitee.sop.sopauth.controller; |
||||
|
||||
import com.gitee.sop.servercommon.annotation.ApiMapping; |
||||
import com.gitee.sop.sopauth.auth.FetchTokenParam; |
||||
import com.gitee.sop.sopauth.auth.FetchTokenResult; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Config; |
||||
import com.gitee.sop.sopauth.auth.OAuth2Service; |
||||
import org.apache.oltu.oauth2.common.exception.OAuthSystemException; |
||||
import org.apache.oltu.oauth2.common.message.OAuthResponse; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.ui.ModelMap; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
import java.net.URI; |
||||
import java.net.URISyntaxException; |
||||
|
||||
/** |
||||
* 授权认证 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Controller |
||||
@RequestMapping("oauth2") |
||||
public class OAuth2Controller { |
||||
|
||||
@Autowired |
||||
private OAuth2Service oAuth2Service; |
||||
|
||||
// 第一步:授权URL拼装
|
||||
// https://localhost:8087/oauth2/appToAppAuth?app_id=2019032617262200001&redirect_uri=http%3a%2f%2flocalhost%3a8087%2foauth2callback
|
||||
@GetMapping("appToAppAuth") |
||||
public String appToAppAuth(HttpServletRequest request, ModelMap modelMap) { |
||||
String app_id = request.getParameter("app_id"); |
||||
String redirect_uri = request.getParameter("redirect_uri"); |
||||
modelMap.put("response_type", "code"); |
||||
modelMap.put("client_id", app_id); |
||||
modelMap.put("redirect_uri", redirect_uri); |
||||
return "oauth2login"; |
||||
} |
||||
|
||||
/** |
||||
* 第二步:点击登录,到这里拿code |
||||
* oauth2认证获取code |
||||
* @param request |
||||
* @param resp |
||||
* @return 返回code |
||||
* @throws URISyntaxException |
||||
* @throws OAuthSystemException |
||||
*/ |
||||
@RequestMapping("authorize") |
||||
public Object authorize(HttpServletRequest request, HttpServletResponse resp) throws URISyntaxException, OAuthSystemException { |
||||
OAuthResponse response = oAuth2Service.authorize(request, resp, OAuth2Config.getInstance()); |
||||
if(response == null) { |
||||
return null; |
||||
} |
||||
HttpHeaders headers = new HttpHeaders(); |
||||
headers.setLocation(new URI(response.getLocationUri())); |
||||
return new ResponseEntity<String>(headers, HttpStatus.valueOf(response.getResponseStatus())); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 第三步,通过code获取token |
||||
* 或者,通过refresh_token换取token |
||||
* @param param |
||||
* @return |
||||
*/ |
||||
@ApiMapping("open.auth.token.app") |
||||
@ResponseBody |
||||
public FetchTokenResult fetchToken(FetchTokenParam param) { |
||||
FetchTokenResult fetchTokenResult = oAuth2Service.accessToken(param, OAuth2Config.getInstance()); |
||||
return fetchTokenResult; |
||||
} |
||||
|
||||
@RequestMapping("login") |
||||
public String oauth2login() { |
||||
return "oauth2login"; |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
package com.gitee.sop.sopauth.entity; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import javax.persistence.Column; |
||||
import javax.persistence.GeneratedValue; |
||||
import javax.persistence.GenerationType; |
||||
import javax.persistence.Id; |
||||
import javax.persistence.Table; |
||||
|
||||
|
||||
/** |
||||
* 表名:isv_info |
||||
* 备注:isv信息表 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Table(name = "isv_info") |
||||
@Data |
||||
public class IsvInfo { |
||||
@Id |
||||
@Column(name = "id") |
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) |
||||
/** 数据库字段:id */ |
||||
private Long id; |
||||
|
||||
/** appKey, 数据库字段:app_key */ |
||||
private String appKey; |
||||
|
||||
/** 1启用,2禁用, 数据库字段:status */ |
||||
private Byte status; |
||||
} |
@ -0,0 +1,50 @@ |
||||
package com.gitee.sop.sopauth.entity; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import com.gitee.sop.sopauth.auth.OpenUser; |
||||
import lombok.Data; |
||||
|
||||
import javax.persistence.Column; |
||||
import javax.persistence.GeneratedValue; |
||||
import javax.persistence.GenerationType; |
||||
import javax.persistence.Id; |
||||
import javax.persistence.Table; |
||||
import java.util.Date; |
||||
|
||||
|
||||
/** |
||||
* 表名:user_info |
||||
* 备注:用户信息表 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Table(name = "user_info") |
||||
@Data |
||||
public class UserInfo implements OpenUser { |
||||
@Id |
||||
@Column(name = "id") |
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) |
||||
/** 数据库字段:id */ |
||||
private Long id; |
||||
|
||||
/** 用户名, 数据库字段:username */ |
||||
private String username; |
||||
|
||||
/** 密码, 数据库字段:password */ |
||||
@JSONField(serialize = false) |
||||
private String password; |
||||
|
||||
/** 昵称, 数据库字段:nickname */ |
||||
private String nickname; |
||||
|
||||
/** 数据库字段:gmt_create */ |
||||
private Date gmtCreate; |
||||
|
||||
/** 数据库字段:gmt_modified */ |
||||
private Date gmtModified; |
||||
|
||||
@Override |
||||
public String getUserId() { |
||||
return String.valueOf(id); |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
package com.gitee.sop.sopauth.mapper; |
||||
|
||||
import com.gitee.fastmybatis.core.mapper.CrudMapper; |
||||
import com.gitee.sop.sopauth.entity.IsvInfo; |
||||
|
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface IsvInfoMapper extends CrudMapper<IsvInfo, Long> { |
||||
} |
@ -0,0 +1,11 @@ |
||||
package com.gitee.sop.sopauth.mapper; |
||||
|
||||
import com.gitee.fastmybatis.core.mapper.CrudMapper; |
||||
import com.gitee.sop.sopauth.entity.UserInfo; |
||||
|
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface UserInfoMapper extends CrudMapper<UserInfo, Long> { |
||||
} |
@ -0,0 +1,26 @@ |
||||
server.port=8087 |
||||
spring.application.name=sop-auth |
||||
|
||||
# ------- 需要改的配置 ------- |
||||
# mysql数据库账号 |
||||
mysql.host=localhost:3306 |
||||
mysql.username=root |
||||
mysql.password=root |
||||
|
||||
# nacos地址 |
||||
nacos.url=127.0.0.1:8848 |
||||
|
||||
# ------- 需要改的配置end ------- |
||||
|
||||
# nacos cloud配置 |
||||
spring.cloud.nacos.discovery.server-addr=${nacos.url} |
||||
|
||||
# 数据库配置 |
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver |
||||
spring.datasource.url=jdbc:mysql://${mysql.host}/sop?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai |
||||
spring.datasource.username=${mysql.username} |
||||
spring.datasource.password=${mysql.password} |
||||
|
||||
spring.thymeleaf.cache=false |
||||
|
||||
logging.level.com.gitee=debug |
@ -0,0 +1 @@ |
||||
spring.profiles.active=dev |
@ -0,0 +1,19 @@ |
||||
<!DOCTYPE html> |
||||
<html xmlns="http://www.w3.org/1999/xhtml" |
||||
xmlns:th="http://www.thymeleaf.org" |
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> |
||||
<head> |
||||
<title>用户登录</title> |
||||
</head> |
||||
<body> |
||||
<div th:if="${error}" style="color: red;"><span th:text="${error}"></span></div> |
||||
<form th:action="@{/oauth2/authorize}" method="post"> |
||||
<input name="response_type" type="hidden" th:value="${response_type}"/> |
||||
<input name="client_id" type="hidden" th:value="${client_id}"/> |
||||
<input name="redirect_uri" type="hidden" th:value="${redirect_uri}"/> |
||||
用户名: <input type="text" name="username" value="zhangsan"/> <br/> |
||||
密码: <input type="password" name="password" value="123456"/> <br/> |
||||
<button type="submit">登录并授权</button> |
||||
</form> |
||||
</body> |
||||
</html> |
@ -0,0 +1,40 @@ |
||||
package com.gitee.sop.sopauth; |
||||
|
||||
import com.gitee.sop.sdk.client.OpenClient; |
||||
import com.gitee.sop.sdk.model.OpenAuthTokenAppModel; |
||||
import com.gitee.sop.sdk.request.OpenAuthTokenAppRequest; |
||||
import com.gitee.sop.sdk.response.OpenAuthTokenAppResponse; |
||||
import junit.framework.TestCase; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class RefreshTokenTest extends TestCase { |
||||
String url = "http://localhost:8081"; |
||||
String appId = "2019032617262200001"; |
||||
// 平台提供的私钥
|
||||
String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ="; |
||||
|
||||
OpenClient openClient = new OpenClient(url, appId, privateKey); |
||||
|
||||
/** |
||||
* 根据refreshToken换取token |
||||
*/ |
||||
public void testRefreshToken() { |
||||
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest(); |
||||
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel(); |
||||
model.setGrant_type("refresh_token"); |
||||
model.setRefresh_token("c9e4003c06fe59066eed73d64ea074ca"); |
||||
request.setBizModel(model); |
||||
|
||||
OpenAuthTokenAppResponse response = openClient.execute(request); |
||||
if (response.isSuccess()) { |
||||
// 成功拿到token,开发者在这里保存token信息
|
||||
// 后续使用token进行接口访问
|
||||
log.info("换取token成功,body:{}", response.getBody()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,16 @@ |
||||
package com.gitee.sop.sopauth; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
|
||||
@RunWith(SpringRunner.class) |
||||
@SpringBootTest |
||||
public class SopAuthApplicationTests { |
||||
|
||||
@Test |
||||
public void contextLoads() { |
||||
} |
||||
|
||||
} |
@ -0,0 +1,17 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class GatewayFilterDefinition { |
||||
/** Filter Name */ |
||||
private String name; |
||||
/** 对应的路由规则 */ |
||||
private Map<String, String> args = new LinkedHashMap<>(); |
||||
} |
@ -0,0 +1,42 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import javax.validation.ValidationException; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class GatewayPredicateDefinition { |
||||
public static final String GEN_KEY = "_genkey_"; |
||||
/** 断言对应的Name */ |
||||
private String name; |
||||
/** 配置的断言规则 */ |
||||
private Map<String, String> args = new LinkedHashMap<>(); |
||||
|
||||
public GatewayPredicateDefinition() { |
||||
} |
||||
|
||||
public GatewayPredicateDefinition(String text) { |
||||
int eqIdx = text.indexOf(61); |
||||
if (eqIdx <= 0) { |
||||
throw new ValidationException("Unable to parse GatewayPredicateDefinition text '" + text + "', must be of the form name=value"); |
||||
} else { |
||||
this.setName(text.substring(0, eqIdx)); |
||||
String[] params = StringUtils.tokenizeToStringArray(text.substring(eqIdx + 1), ","); |
||||
|
||||
for(int i = 0; i < params.length; ++i) { |
||||
this.args.put(generateName(i), params[i]); |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
public static String generateName(int i) { |
||||
return GEN_KEY + i; |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class NacosConfigs { |
||||
|
||||
public static final String GROUP_CHANNEL = "sop:channel"; |
||||
|
||||
public static final String GROUP_ROUTE = "sop:route"; |
||||
|
||||
public static final String DATA_ID_GRAY = "com.gitee.sop.channel.gray"; |
||||
|
||||
public static final String DATA_ID_IP_BLACKLIST = "com.gitee.sop.channel.ipblacklist"; |
||||
|
||||
public static final String DATA_ID_ISV = "com.gitee.sop.channel.isv"; |
||||
|
||||
public static final String DATA_ID_ROUTE_PERMISSION = "com.gitee.sop.channel.routepermission"; |
||||
|
||||
public static final String DATA_ID_LIMIT_CONFIG = "com.gitee.sop.channel.limitconfig"; |
||||
|
||||
public static final String DATA_ID_ROUTE_CONFIG = "com.gitee.sop.channel.routeconfig"; |
||||
|
||||
private static final String DATA_ID_TPL = "com.gitee.sop.route.%s"; |
||||
|
||||
public static String getRouteDataId(String serviceId) { |
||||
return String.format(DATA_ID_TPL, serviceId); |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class RouteDefinition { |
||||
/** |
||||
* 路由的Id(接口名+版本号),确保此id全局唯一 |
||||
*/ |
||||
private String id; |
||||
|
||||
/** |
||||
* 接口名 |
||||
*/ |
||||
private String name; |
||||
|
||||
/** |
||||
* 版本号 |
||||
*/ |
||||
private String version; |
||||
|
||||
/** |
||||
* 路由断言集合配置 |
||||
*/ |
||||
private List<GatewayPredicateDefinition> predicates = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由过滤器集合配置 |
||||
*/ |
||||
private List<GatewayFilterDefinition> filters = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 路由规则转发的目标uri |
||||
*/ |
||||
private String uri; |
||||
|
||||
/** |
||||
* uri后面跟的path |
||||
*/ |
||||
private String path; |
||||
|
||||
/** |
||||
* 路由执行的顺序 |
||||
*/ |
||||
private int order = 0; |
||||
|
||||
/** |
||||
* 是否忽略验证,业务参数验证除外 |
||||
*/ |
||||
private int ignoreValidate; |
||||
|
||||
/** |
||||
* 状态,0:待审核,1:启用,2:禁用 |
||||
*/ |
||||
private int status = 1; |
||||
|
||||
/** |
||||
* 是否合并结果 |
||||
*/ |
||||
private int mergeResult; |
||||
|
||||
/** |
||||
* 是否需要授权才能访问 |
||||
*/ |
||||
private int permission; |
||||
} |
@ -0,0 +1,38 @@ |
||||
package com.gitee.sop.gatewaycommon.bean; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import lombok.Data; |
||||
|
||||
import java.util.Date; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class ServiceRouteInfo { |
||||
|
||||
/** |
||||
* 服务名称,对应spring.application.name |
||||
*/ |
||||
private String serviceId; |
||||
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss") |
||||
private Date createTime = new Date(); |
||||
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss") |
||||
private Date updateTime = new Date(); |
||||
|
||||
private String description; |
||||
|
||||
/** |
||||
* 路由信息md5,md5(sort(routeIdList)) |
||||
*/ |
||||
private String md5; |
||||
|
||||
private List<RouteDefinition> routeDefinitionList; |
||||
|
||||
public String fetchServiceIdLowerCase() { |
||||
return serviceId.toLowerCase(); |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.filter; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||
import com.gitee.sop.gatewaycommon.manager.EnvGrayManager; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext; |
||||
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
||||
import org.springframework.cloud.gateway.filter.GlobalFilter; |
||||
import org.springframework.core.Ordered; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class EnvGrayFilter implements GlobalFilter, Ordered { |
||||
|
||||
@Autowired |
||||
private EnvGrayManager envGrayManager; |
||||
|
||||
@Override |
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { |
||||
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange); |
||||
String nameVersion = apiParam.fetchNameVersion(); |
||||
TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(nameVersion); |
||||
if (targetRoute == null) { |
||||
return null; |
||||
} |
||||
String serviceId = targetRoute.getServiceRouteInfo().fetchServiceIdLowerCase(); |
||||
// 如果服务在灰度阶段,返回一个灰度版本号
|
||||
String version = envGrayManager.getVersion(serviceId, nameVersion); |
||||
if (version != null && envGrayManager.containsKey(serviceId, apiParam.fetchAppKey())) { |
||||
ServerWebExchange serverWebExchange = ServerWebExchangeUtil.addHeaders(exchange, httpHeaders -> httpHeaders.set(ParamNames.HEADER_VERSION_NAME, version)); |
||||
return chain.filter(serverWebExchange); |
||||
} |
||||
return chain.filter(exchange); |
||||
} |
||||
|
||||
@Override |
||||
public int getOrder() { |
||||
return Orders.ENV_GRAY_FILTER_ORDER; |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
package com.gitee.sop.gatewaycommon.gateway.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.cloud.gateway.filter.FilterDefinition; |
||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; |
||||
|
||||
import java.net.URI; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class GatewayRouteCache extends BaseRouteCache<GatewayTargetRoute> { |
||||
|
||||
public GatewayRouteCache(RouteRepository<GatewayTargetRoute> routeRepository) { |
||||
super(routeRepository); |
||||
} |
||||
|
||||
@Override |
||||
protected GatewayTargetRoute buildTargetRoute(ServiceRouteInfo serviceRouteInfo, RouteDefinition routeDefinition) { |
||||
org.springframework.cloud.gateway.route.RouteDefinition targetRoute = new org.springframework.cloud.gateway.route.RouteDefinition(); |
||||
targetRoute.setId(routeDefinition.getId()); |
||||
targetRoute.setUri(URI.create(routeDefinition.getUri() + "#" + routeDefinition.getPath())); |
||||
targetRoute.setOrder(routeDefinition.getOrder()); |
||||
// 添加过滤器
|
||||
List<FilterDefinition> filterDefinitionList = routeDefinition.getFilters() |
||||
.stream() |
||||
.map(filter -> { |
||||
FilterDefinition filterDefinition = new FilterDefinition(); |
||||
BeanUtils.copyProperties(filter, filterDefinition); |
||||
return filterDefinition; |
||||
}) |
||||
.collect(Collectors.toList()); |
||||
|
||||
|
||||
LinkedList<PredicateDefinition> predicateDefinitionList = routeDefinition.getPredicates() |
||||
.stream() |
||||
.map(predicate -> { |
||||
PredicateDefinition predicateDefinition = new PredicateDefinition(); |
||||
BeanUtils.copyProperties(predicate, predicateDefinition); |
||||
return predicateDefinition; |
||||
}) |
||||
.collect(Collectors.toCollection(LinkedList::new)); |
||||
// 下面两个自定义的断言添加到顶部,注意:ReadBody需要放在最前面
|
||||
// 对应断言:
|
||||
// NameVersion -> com.gitee.sop.gatewaycommon.gateway.route.NameVersionRoutePredicateFactory
|
||||
// ReadBody -> com.gitee.sop.gatewaycommon.gateway.route.ReadBodyRoutePredicateFactory
|
||||
predicateDefinitionList.addFirst(new PredicateDefinition("NameVersion=" + routeDefinition.getId())); |
||||
predicateDefinitionList.addFirst(new PredicateDefinition("ReadBody=")); |
||||
|
||||
targetRoute.setFilters(filterDefinitionList); |
||||
targetRoute.setPredicates(predicateDefinitionList); |
||||
return new GatewayTargetRoute(serviceRouteInfo, routeDefinition, targetRoute); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,70 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.gatewaycommon.bean.RouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public abstract class BaseRouteCache<T extends TargetRoute> implements RouteLoader { |
||||
|
||||
/** |
||||
* KEY:serviceId, value: md5 |
||||
*/ |
||||
private Map<String, String> serviceIdMd5Map = new HashMap<>(); |
||||
|
||||
private RouteRepository<T> routeRepository; |
||||
|
||||
/** |
||||
* 构建目标路由对象,zuul和gateway定义的路由对象 |
||||
* |
||||
* @param serviceRouteInfo 路由服务对象 |
||||
* @param gatewayRouteDefinition 路由对象 |
||||
* @return 返回目标路由对象 |
||||
*/ |
||||
protected abstract T buildTargetRoute(ServiceRouteInfo serviceRouteInfo, RouteDefinition gatewayRouteDefinition); |
||||
|
||||
public BaseRouteCache(RouteRepository<T> routeRepository) { |
||||
this.routeRepository = routeRepository; |
||||
} |
||||
|
||||
@Override |
||||
public void load(ServiceRouteInfo serviceRouteInfo, Consumer<Object> callback) { |
||||
try { |
||||
String serviceId = serviceRouteInfo.getServiceId(); |
||||
String newMd5 = serviceRouteInfo.getMd5(); |
||||
String oldMd5 = serviceIdMd5Map.get(serviceId); |
||||
if (Objects.equals(newMd5, oldMd5)) { |
||||
return; |
||||
} |
||||
serviceIdMd5Map.put(serviceId, newMd5); |
||||
List<RouteDefinition> routeDefinitionList = serviceRouteInfo.getRouteDefinitionList(); |
||||
for (RouteDefinition routeDefinition : routeDefinitionList) { |
||||
T targetRoute = this.buildTargetRoute(serviceRouteInfo, routeDefinition); |
||||
routeRepository.add(targetRoute); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("新增路由:{}", JSON.toJSONString(routeDefinition)); |
||||
} |
||||
} |
||||
callback.accept(null); |
||||
} catch (Exception e) { |
||||
log.error("加载路由信息失败,serviceRouteInfo:{}", serviceRouteInfo, e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void remove(String serviceId) { |
||||
serviceIdMd5Map.remove(serviceId); |
||||
routeRepository.deleteAll(serviceId); |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
|
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface RouteLoader { |
||||
/** |
||||
* 加载路由 |
||||
* |
||||
* @param serviceRouteInfo 服务路由信息 |
||||
* @param callback 加载成功后回调 |
||||
*/ |
||||
void load(ServiceRouteInfo serviceRouteInfo, Consumer<Object> callback); |
||||
|
||||
/** |
||||
* 移除某个微服务下的所有路由信息 |
||||
* |
||||
* @param serviceId 服务id |
||||
*/ |
||||
void remove(String serviceId); |
||||
} |
@ -0,0 +1,136 @@ |
||||
package com.gitee.sop.gatewaycommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.nacos.api.annotation.NacosInjected; |
||||
import com.alibaba.nacos.api.config.ConfigService; |
||||
import com.alibaba.nacos.api.exception.NacosException; |
||||
import com.alibaba.nacos.api.naming.NamingService; |
||||
import com.alibaba.nacos.api.naming.pojo.Instance; |
||||
import com.alibaba.nacos.api.naming.pojo.ServiceInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.NacosConfigs; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties; |
||||
import org.springframework.context.ApplicationEvent; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.web.client.DefaultResponseErrorHandler; |
||||
import org.springframework.web.client.RestTemplate; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* 发现新服务,更新路由信息 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ServiceRoutesLoader<T extends TargetRoute> { |
||||
|
||||
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq"; |
||||
|
||||
private static final int FIVE_SECONDS = 1000 * 5; |
||||
|
||||
@Autowired |
||||
private NacosDiscoveryProperties nacosDiscoveryProperties; |
||||
|
||||
@NacosInjected |
||||
private ConfigService configService; |
||||
|
||||
@Autowired |
||||
private BaseRouteCache<T> baseRouteCache; |
||||
|
||||
private RestTemplate restTemplate = new RestTemplate(); |
||||
|
||||
private Map<String, Long> updateTimeMap = new HashMap<>(16); |
||||
|
||||
public ServiceRoutesLoader() { |
||||
// 解决statusCode不等于200,就抛异常问题
|
||||
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { |
||||
@Override |
||||
protected boolean hasError(HttpStatus statusCode) { |
||||
return statusCode == null; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public synchronized void load(ApplicationEvent event) { |
||||
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); |
||||
List<ServiceInfo> subscribes = null; |
||||
try { |
||||
subscribes = namingService.getSubscribeServices(); |
||||
} catch (NacosException e) { |
||||
log.error("namingService.getSubscribeServices()错误", e); |
||||
} |
||||
if (CollectionUtils.isEmpty(subscribes)) { |
||||
return; |
||||
} |
||||
// subscribe
|
||||
String thisServiceId = nacosDiscoveryProperties.getService(); |
||||
// ConfigService configService = nacosConfigProperties.configServiceInstance();
|
||||
for (ServiceInfo serviceInfo : subscribes) { |
||||
String serviceName = serviceInfo.getName(); |
||||
// 如果是本机服务,跳过
|
||||
if (Objects.equals(thisServiceId, serviceName)) { |
||||
continue; |
||||
} |
||||
// nacos会不停的触发事件,这里做了一层拦截
|
||||
// 同一个serviceId5秒内允许访问一次
|
||||
Long lastUpdateTime = updateTimeMap.getOrDefault(serviceName, 0L); |
||||
long now = System.currentTimeMillis(); |
||||
if (now - lastUpdateTime < FIVE_SECONDS) { |
||||
continue; |
||||
} |
||||
updateTimeMap.put(serviceName, now); |
||||
try { |
||||
String dataId = NacosConfigs.getRouteDataId(serviceName); |
||||
String groupId = NacosConfigs.GROUP_ROUTE; |
||||
List<Instance> allInstances = namingService.getAllInstances(serviceName); |
||||
if (CollectionUtils.isEmpty(allInstances)) { |
||||
// 如果没有服务列表,则删除所有路由信息
|
||||
baseRouteCache.remove(serviceName); |
||||
configService.removeConfig(dataId, groupId); |
||||
} else { |
||||
for (Instance instance : allInstances) { |
||||
String url = getRouteRequestUrl(instance); |
||||
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); |
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) { |
||||
String body = responseEntity.getBody(); |
||||
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(body, ServiceRouteInfo.class); |
||||
baseRouteCache.load(serviceRouteInfo, callback -> { |
||||
try { |
||||
log.info("加载服务路由,serviceId:{}, instance:{}",serviceName, instance); |
||||
configService.publishConfig(dataId, groupId, body); |
||||
} catch (NacosException e) { |
||||
log.error("nacos推送失败,serviceId:{}, instance:{}",serviceName, instance); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
} catch (NacosException e) { |
||||
log.error("选择服务实例失败,serviceName:{}", serviceName, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static String getRouteRequestUrl(Instance instance) { |
||||
String query = buildQuery(SECRET); |
||||
return "http://" + instance.getIp() + ":" + instance.getPort() + "/sop/routes" + query; |
||||
} |
||||
|
||||
private static String buildQuery(String secret) { |
||||
String time = String.valueOf(System.currentTimeMillis()); |
||||
String source = secret + time + secret; |
||||
String sign = DigestUtils.md5DigestAsHex(source.getBytes()); |
||||
return "?time=" + time + "&sign=" + sign; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,37 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul; |
||||
|
||||
import com.netflix.util.Pair; |
||||
import com.netflix.zuul.context.RequestContext; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.springframework.http.HttpHeaders; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class RequestContextUtil { |
||||
|
||||
/** |
||||
* 获取微服务端传递过来的header |
||||
* |
||||
* @param name header名 |
||||
* @return 返回value,没有返回null |
||||
*/ |
||||
public static String getZuulResponseHeader(RequestContext requestContext, String name) { |
||||
List<Pair<String, String>> zuulResponseHeaders = requestContext.getZuulResponseHeaders(); |
||||
return zuulResponseHeaders.stream() |
||||
.filter(pair -> StringUtils.containsIgnoreCase(pair.first(), name)) |
||||
.findFirst() |
||||
.map(Pair::second) |
||||
.orElse(null); |
||||
} |
||||
|
||||
/** |
||||
* 获取微服务端的content-type |
||||
* @return 返回content-type |
||||
*/ |
||||
public static String getZuulContentType(RequestContext requestContext) { |
||||
return getZuulResponseHeader(requestContext, HttpHeaders.CONTENT_TYPE); |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.filter; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||
import com.gitee.sop.gatewaycommon.manager.EnvGrayManager; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext; |
||||
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
||||
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||
import com.netflix.zuul.context.RequestContext; |
||||
import com.netflix.zuul.exception.ZuulException; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
||||
/** |
||||
* 灰度发布判断,改变版本号 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
public class PreEnvGrayFilter extends BaseZuulFilter { |
||||
|
||||
@Autowired |
||||
private EnvGrayManager envGrayManager; |
||||
|
||||
@Override |
||||
protected FilterType getFilterType() { |
||||
return FilterType.PRE; |
||||
} |
||||
|
||||
@Override |
||||
protected int getFilterOrder() { |
||||
return PRE_ENV_GRAY_FILTER_ORDER; |
||||
} |
||||
|
||||
@Override |
||||
protected Object doRun(RequestContext requestContext) throws ZuulException { |
||||
ApiParam apiParam = ZuulContext.getApiParam(); |
||||
String nameVersion = apiParam.fetchNameVersion(); |
||||
TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(nameVersion); |
||||
if (targetRoute == null) { |
||||
return null; |
||||
} |
||||
String serviceId = targetRoute.getServiceRouteInfo().fetchServiceIdLowerCase(); |
||||
// 如果服务在灰度阶段,返回一个灰度版本号
|
||||
String version = envGrayManager.getVersion(serviceId, nameVersion); |
||||
if (version != null && envGrayManager.containsKey(serviceId, apiParam.fetchAppKey())) { |
||||
requestContext.addZuulRequestHeader(ParamNames.HEADER_VERSION_NAME, version); |
||||
} |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
package com.gitee.sop.gatewaycommon.zuul.route; |
||||
|
||||
import com.gitee.sop.gatewaycommon.bean.RouteDefinition; |
||||
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo; |
||||
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache; |
||||
import com.gitee.sop.gatewaycommon.manager.RouteRepository; |
||||
import com.gitee.sop.gatewaycommon.util.RouteUtil; |
||||
import org.springframework.cloud.netflix.zuul.filters.Route; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ZuulRouteCache extends BaseRouteCache<ZuulTargetRoute> { |
||||
|
||||
/** 路由重试 */ |
||||
private static final boolean RETRYABLE = true; |
||||
|
||||
public ZuulRouteCache(RouteRepository<ZuulTargetRoute> routeRepository) { |
||||
super(routeRepository); |
||||
} |
||||
|
||||
@Override |
||||
protected ZuulTargetRoute buildTargetRoute(ServiceRouteInfo serviceRouteInfo, RouteDefinition gatewayRouteDefinition) { |
||||
Route route = new Route( |
||||
gatewayRouteDefinition.getId() |
||||
, gatewayRouteDefinition.getPath() |
||||
, RouteUtil.getZuulLocation(gatewayRouteDefinition.getUri()) |
||||
, "" |
||||
, RETRYABLE |
||||
, null |
||||
); |
||||
return new ZuulTargetRoute(serviceRouteInfo, gatewayRouteDefinition, route); |
||||
} |
||||
} |
@ -0,0 +1,91 @@ |
||||
package com.gitee.sop.servercommon.easyopen; |
||||
|
||||
import com.gitee.easyopen.annotation.Api; |
||||
import com.gitee.easyopen.util.ReflectionUtil; |
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.manager.ApiMetaBuilder; |
||||
import org.apache.commons.lang3.BooleanUtils; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.core.annotation.AnnotationUtils; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class EasyopenApiMetaBuilder extends ApiMetaBuilder { |
||||
|
||||
/** IndexController上面的@RequestMapping()中的值,如:/api */ |
||||
private final String prefixPath; |
||||
|
||||
private final String contextPath; |
||||
|
||||
private ApplicationContext applicationContext; |
||||
|
||||
private Environment environment; |
||||
|
||||
public EasyopenApiMetaBuilder(ApplicationContext applicationContext, Environment environment) { |
||||
this.applicationContext = applicationContext; |
||||
this.environment = environment; |
||||
String path = environment.getProperty("easyopen.prefix-path"); |
||||
if (path == null) { |
||||
throw new IllegalArgumentException("请在application.propertis中设置easyopen.prefix-path属性,填IndexController上面的@RequestMapping()中的值"); |
||||
} |
||||
if (!path.startsWith("/")) { |
||||
path = "/" + path; |
||||
} |
||||
this.prefixPath = path; |
||||
this.contextPath = environment.getProperty("server.servlet.context-path", "/"); |
||||
} |
||||
|
||||
@Override |
||||
protected List<ServiceApiInfo.ApiMeta> buildApiMetaList(RequestMappingHandlerMapping requestMappingHandlerMapping) { |
||||
String[] apiServiceNames = ReflectionUtil.findApiServiceNames(applicationContext); |
||||
List<ServiceApiInfo.ApiMeta> apiMetaList = new ArrayList<>(); |
||||
for (String apiServiceName : apiServiceNames) { |
||||
Object bean = applicationContext.getBean(apiServiceName); |
||||
doWithMethods(bean.getClass(), method -> { |
||||
Api api = AnnotationUtils.findAnnotation(method, Api.class); |
||||
ServiceApiInfo.ApiMeta apiMeta = new ServiceApiInfo.ApiMeta(); |
||||
apiMeta.setName(api.name()); |
||||
apiMeta.setVersion(api.version()); |
||||
apiMeta.setIgnoreValidate(BooleanUtils.toInteger(api.ignoreValidate())); |
||||
// 对结果不合并
|
||||
apiMeta.setMergeResult(BooleanUtils.toInteger(false)); |
||||
// /api/goods.get/
|
||||
String servletPath = this.buildPath(api); |
||||
apiMeta.setPath(servletPath); |
||||
apiMetaList.add(apiMeta); |
||||
}); |
||||
} |
||||
return apiMetaList; |
||||
} |
||||
|
||||
protected void doWithMethods(Class<?> clazz, Consumer<Method> consumer) { |
||||
// Keep backing up the inheritance hierarchy.
|
||||
Method[] methods = clazz.getDeclaredMethods(); |
||||
for (Method method : methods) { |
||||
boolean match = !method.isSynthetic() && AnnotationUtils.findAnnotation(method, Api.class) != null; |
||||
if (match) { |
||||
consumer.accept(method); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected String buildPath(Api api) { |
||||
// /api/goods.get/
|
||||
String servletPath = prefixPath + "/" + api.name() + "/"; |
||||
String version = api.version(); |
||||
if (StringUtils.hasLength(version)) { |
||||
// /api/goods.get/1.0/
|
||||
servletPath = servletPath + version + "/"; |
||||
} |
||||
return contextPath + prefixPath + servletPath; |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
package com.gitee.sop.servercommon.easyopen; |
||||
|
||||
import com.gitee.easyopen.ApiContext; |
||||
import com.gitee.sop.servercommon.configuration.BaseServiceConfiguration; |
||||
import com.gitee.sop.servercommon.manager.ServiceRouteController; |
||||
import org.springframework.context.annotation.Bean; |
||||
|
||||
/** |
||||
* 提供给easyopen项目使用 |
||||
* |
||||
* @author tanghc |
||||
*/ |
||||
public class EasyopenServiceConfiguration extends BaseServiceConfiguration { |
||||
|
||||
static { |
||||
ApiContext.getApiConfig().setIgnoreValidate(true); |
||||
} |
||||
|
||||
@Bean |
||||
ServiceRouteController serviceRouteController() { |
||||
return new EasyopenServiceRouteController(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,27 @@ |
||||
package com.gitee.sop.servercommon.easyopen; |
||||
|
||||
import com.gitee.sop.servercommon.manager.ApiMetaBuilder; |
||||
import com.gitee.sop.servercommon.manager.ServiceRouteController; |
||||
import org.springframework.beans.BeansException; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@RestController |
||||
public class EasyopenServiceRouteController extends ServiceRouteController implements ApplicationContextAware { |
||||
|
||||
private ApplicationContext applicationContext; |
||||
|
||||
@Override |
||||
protected ApiMetaBuilder getApiMetaBuilder() { |
||||
return new EasyopenApiMetaBuilder(applicationContext, getEnvironment()); |
||||
} |
||||
|
||||
@Override |
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { |
||||
this.applicationContext = applicationContext; |
||||
} |
||||
} |
@ -0,0 +1,100 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.bean.ServiceConfig; |
||||
import com.gitee.sop.servercommon.mapping.ApiMappingInfo; |
||||
import com.gitee.sop.servercommon.mapping.ApiMappingRequestCondition; |
||||
import com.gitee.sop.servercommon.mapping.RouteUtil; |
||||
import org.apache.commons.lang3.BooleanUtils; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
import org.springframework.web.servlet.mvc.condition.RequestCondition; |
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class ApiMetaBuilder { |
||||
|
||||
/** 接口名规则:允许字母、数字、点、下划线 */ |
||||
private static final String REGEX_API_NAME = "^[a-zA-Z0-9\\.\\_\\-]+$"; |
||||
|
||||
public ServiceApiInfo getServiceApiInfo(String serviceId, RequestMappingHandlerMapping requestMappingHandlerMapping) { |
||||
if (serviceId == null) { |
||||
throw new IllegalArgumentException("请在application.properties中指定spring.application.name属性"); |
||||
} |
||||
List<ServiceApiInfo.ApiMeta> apis = this.buildApiMetaList(requestMappingHandlerMapping); |
||||
// 排序
|
||||
apis.sort(Comparator.comparing(ServiceApiInfo.ApiMeta::fetchNameVersion)); |
||||
|
||||
ServiceApiInfo serviceApiInfo = new ServiceApiInfo(); |
||||
serviceApiInfo.setServiceId(serviceId); |
||||
serviceApiInfo.setApis(apis); |
||||
|
||||
return serviceApiInfo; |
||||
} |
||||
|
||||
protected List<ServiceApiInfo.ApiMeta> buildApiMetaList(RequestMappingHandlerMapping requestMappingHandlerMapping) { |
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods(); |
||||
Set<RequestMappingInfo> requestMappingInfos = handlerMethods.keySet(); |
||||
List<String> store = new ArrayList<>(); |
||||
List<ServiceApiInfo.ApiMeta> apis = new ArrayList<>(requestMappingInfos.size()); |
||||
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethodEntry : handlerMethods.entrySet()) { |
||||
ServiceApiInfo.ApiMeta apiMeta = this.buildApiMeta(handlerMethodEntry); |
||||
if (apiMeta == null) { |
||||
continue; |
||||
} |
||||
String key = apiMeta.fetchNameVersion(); |
||||
if (store.contains(key)) { |
||||
throw new IllegalArgumentException("重复申明接口,请检查path和version,path:" + apiMeta.getPath() + ", version:" + apiMeta.getVersion()); |
||||
} else { |
||||
store.add(key); |
||||
} |
||||
apis.add(apiMeta); |
||||
} |
||||
return apis; |
||||
} |
||||
|
||||
protected ServiceApiInfo.ApiMeta buildApiMeta(Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethodEntry) { |
||||
RequestMappingInfo requestMappingInfo = handlerMethodEntry.getKey(); |
||||
Set<String> patterns = requestMappingInfo.getPatternsCondition().getPatterns(); |
||||
RequestCondition<?> customCondition = requestMappingInfo.getCustomCondition(); |
||||
if (customCondition instanceof ApiMappingRequestCondition) { |
||||
ApiMappingRequestCondition condition = (ApiMappingRequestCondition) customCondition; |
||||
ApiMappingInfo apiMappingInfo = condition.getApiMappingInfo(); |
||||
String name = apiMappingInfo.getName(); |
||||
String version = apiMappingInfo.getVersion(); |
||||
// 方法完整的path,如: /goods/listGoods,/users/user/get
|
||||
String path = patterns.iterator().next(); |
||||
// 不是ApiMapping注解的接口,name属性是null
|
||||
if (name == null || ServiceConfig.getInstance().isWebappMode()) { |
||||
name = buildName(path); |
||||
} |
||||
this.checkApiName(name); |
||||
ServiceApiInfo.ApiMeta apiMeta = new ServiceApiInfo.ApiMeta(name, path, version); |
||||
apiMeta.setIgnoreValidate(BooleanUtils.toInteger(apiMappingInfo.isIgnoreValidate())); |
||||
apiMeta.setMergeResult(BooleanUtils.toInteger(apiMappingInfo.isMergeResult())); |
||||
apiMeta.setPermission(BooleanUtils.toInteger(apiMappingInfo.isPermission())); |
||||
return apiMeta; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected void checkApiName(String name) { |
||||
if (!name.matches(REGEX_API_NAME)) { |
||||
throw new IllegalArgumentException("接口名称只允许【字母、数字、点(.)、下划线(_)、减号(-)】,错误接口:" + name); |
||||
} |
||||
} |
||||
|
||||
|
||||
protected String buildName(String path) { |
||||
return RouteUtil.buildApiName(path); |
||||
} |
||||
} |
@ -0,0 +1,55 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.route.ServiceRouteInfo; |
||||
import com.gitee.sop.servercommon.util.OpenUtil; |
||||
import lombok.Getter; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
@Getter |
||||
@RestController |
||||
public class ServiceRouteController { |
||||
|
||||
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq"; |
||||
|
||||
@Autowired |
||||
private RequestMappingHandlerMapping requestMappingHandlerMapping; |
||||
|
||||
@Autowired |
||||
private Environment environment; |
||||
|
||||
|
||||
@RequestMapping("/sop/routes") |
||||
public ServiceRouteInfo listRoutes(HttpServletRequest request, HttpServletResponse response) { |
||||
if (!OpenUtil.validateSimpleSign(request, SECRET)) { |
||||
log.error("签名验证失败, params:{}", request.getQueryString()); |
||||
return null; |
||||
} |
||||
return getServiceRouteInfo(request, response); |
||||
} |
||||
|
||||
protected ApiMetaBuilder getApiMetaBuilder() { |
||||
return new ApiMetaBuilder(); |
||||
} |
||||
|
||||
protected ServiceRouteInfo getServiceRouteInfo(HttpServletRequest request, HttpServletResponse response) { |
||||
String serviceId = environment.getProperty("spring.application.name"); |
||||
ApiMetaBuilder apiMetaBuilder = getApiMetaBuilder(); |
||||
ServiceApiInfo serviceApiInfo = apiMetaBuilder.getServiceApiInfo(serviceId, requestMappingHandlerMapping); |
||||
ServiceRouteInfoBuilder serviceRouteInfoBuilder = new ServiceRouteInfoBuilder(environment); |
||||
return serviceRouteInfoBuilder.build(serviceApiInfo); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,134 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.servercommon.bean.ServiceApiInfo; |
||||
import com.gitee.sop.servercommon.route.RouteDefinition; |
||||
import com.gitee.sop.servercommon.route.ServiceRouteInfo; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.DigestUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class ServiceRouteInfoBuilder { |
||||
|
||||
/** |
||||
* 网关对应的LoadBalance协议 |
||||
*/ |
||||
private static final String PROTOCOL_LOAD_BALANCE = "lb://"; |
||||
|
||||
private static final String PATH_SPLIT = "/"; |
||||
|
||||
private static final String DEFAULT_CONTEXT_PATH = "/"; |
||||
|
||||
private static ServiceApiInfo.ApiMeta FIRST_API_META = new ServiceApiInfo.ApiMeta("_first_route_", "/", "1.0.0"); |
||||
|
||||
private Environment environment; |
||||
|
||||
public ServiceRouteInfoBuilder(Environment environment) { |
||||
this.environment = environment; |
||||
} |
||||
|
||||
public ServiceRouteInfo build(ServiceApiInfo serviceApiInfo) { |
||||
return this.buildServiceGatewayInfo(serviceApiInfo); |
||||
} |
||||
|
||||
/** |
||||
* 构建接口信息,符合spring cloud gateway的格式 |
||||
* |
||||
* @param serviceApiInfo 服务接口信息 |
||||
* @return 返回服务路由信息 |
||||
*/ |
||||
protected ServiceRouteInfo buildServiceGatewayInfo(ServiceApiInfo serviceApiInfo) { |
||||
List<ServiceApiInfo.ApiMeta> apis = serviceApiInfo.getApis(); |
||||
List<RouteDefinition> routeDefinitionList = new ArrayList<>(apis.size() + 1); |
||||
// 在第一个位置放一个没用的路由,SpringCloudGateway会从第二个路由开始找,原因不详
|
||||
routeDefinitionList.add(this.getFirstRoute(serviceApiInfo)); |
||||
for (ServiceApiInfo.ApiMeta apiMeta : apis) { |
||||
RouteDefinition gatewayRouteDefinition = this.buildGatewayRouteDefinition(serviceApiInfo, apiMeta); |
||||
routeDefinitionList.add(gatewayRouteDefinition); |
||||
} |
||||
ServiceRouteInfo serviceRouteInfo = new ServiceRouteInfo(); |
||||
serviceRouteInfo.setServiceId(serviceApiInfo.getServiceId()); |
||||
serviceRouteInfo.setRouteDefinitionList(routeDefinitionList); |
||||
String md5 = buildMd5(routeDefinitionList); |
||||
serviceRouteInfo.setMd5(md5); |
||||
return serviceRouteInfo; |
||||
} |
||||
|
||||
/** |
||||
* 添加com.gitee.sop.gatewaycommon.routeDefinition.ReadBodyRoutePredicateFactory,解决form表单获取不到问题 |
||||
* |
||||
* @return 返回路由定义 |
||||
*/ |
||||
protected RouteDefinition getFirstRoute(ServiceApiInfo serviceApiInfo) { |
||||
RouteDefinition firstRoute = this.buildGatewayRouteDefinition(serviceApiInfo, FIRST_API_META); |
||||
firstRoute.setOrder(Integer.MIN_VALUE); |
||||
return firstRoute; |
||||
} |
||||
|
||||
/** |
||||
* 构建路由id MD5 |
||||
* |
||||
* @param routeDefinitionList 路由列表 |
||||
* @return 返回MD5 |
||||
*/ |
||||
protected String buildMd5(List<RouteDefinition> routeDefinitionList) { |
||||
List<String> routeIdList = routeDefinitionList.stream() |
||||
.map(JSON::toJSONString) |
||||
.sorted() |
||||
.collect(Collectors.toList()); |
||||
String md5Source = org.apache.commons.lang3.StringUtils.join(routeIdList, ""); |
||||
return DigestUtils.md5DigestAsHex(md5Source.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
protected RouteDefinition buildGatewayRouteDefinition(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
RouteDefinition routeDefinition = new RouteDefinition(); |
||||
// 唯一id规则:接口名 + 版本号
|
||||
String routeId = apiMeta.fetchNameVersion(); |
||||
this.checkPath(routeId, "接口定义(" + routeId + ")不能有斜杠字符'/'"); |
||||
BeanUtils.copyProperties(apiMeta, routeDefinition); |
||||
routeDefinition.setId(routeId); |
||||
routeDefinition.setFilters(Collections.emptyList()); |
||||
routeDefinition.setPredicates(Collections.emptyList()); |
||||
String uri = this.buildUri(serviceApiInfo, apiMeta); |
||||
String path = this.buildServletPath(serviceApiInfo, apiMeta); |
||||
routeDefinition.setUri(uri); |
||||
routeDefinition.setPath(path); |
||||
return routeDefinition; |
||||
} |
||||
|
||||
protected String buildUri(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
return PROTOCOL_LOAD_BALANCE + serviceApiInfo.getServiceId(); |
||||
} |
||||
|
||||
protected String buildServletPath(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) { |
||||
String contextPath = environment.getProperty("server.servlet.context-path", DEFAULT_CONTEXT_PATH); |
||||
String servletPath = apiMeta.getPath(); |
||||
if (servletPath == null) { |
||||
servletPath = ""; |
||||
} |
||||
servletPath = StringUtils.trimLeadingCharacter(servletPath, '/'); |
||||
return contextPath + servletPath; |
||||
} |
||||
|
||||
private void checkPath(String path, String errorMsg) { |
||||
if (path.contains(PATH_SPLIT)) { |
||||
throw new IllegalArgumentException(errorMsg); |
||||
} |
||||
} |
||||
|
||||
public Environment getEnvironment() { |
||||
return environment; |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
package com.gitee.sop.servercommon.manager; |
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public interface ServiceRouteInfoHandler { |
||||
} |
@ -0,0 +1,71 @@ |
||||
package com.gitee.sop.servercommon.route; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class RouteDefinition { |
||||
/** |
||||
* 路由的Id(接口名+版本号),确保此id全局唯一 |
||||
*/ |
||||
private String id; |
||||
|
||||
/** |
||||
* 接口名 |
||||
*/ |
||||
private String name; |
||||
|
||||
/** |
||||
* 版本号 |
||||
*/ |
||||
private String version; |
||||
|
||||
/** |
||||
* 路由断言集合配置 |
||||
*/ |
||||
private List<GatewayPredicateDefinition> predicates; |
||||
|
||||
/** |
||||
* 路由过滤器集合配置 |
||||
*/ |
||||
private List<GatewayFilterDefinition> filters; |
||||
|
||||
/** |
||||
* 路由规则转发的目标uri |
||||
*/ |
||||
private String uri; |
||||
|
||||
/** |
||||
* uri后面跟的path |
||||
*/ |
||||
private String path; |
||||
|
||||
/** |
||||
* 路由执行的顺序 |
||||
*/ |
||||
private int order = 0; |
||||
|
||||
/** |
||||
* 是否忽略验证,业务参数验证除外 |
||||
*/ |
||||
private int ignoreValidate; |
||||
|
||||
/** |
||||
* 状态,0:待审核,1:启用,2:禁用 |
||||
*/ |
||||
private int status = 1; |
||||
|
||||
/** |
||||
* 是否合并结果 |
||||
*/ |
||||
private int mergeResult; |
||||
|
||||
/** |
||||
* 是否需要授权才能访问 |
||||
*/ |
||||
private int permission; |
||||
} |
@ -0,0 +1,7 @@ |
||||
server.port=2121 |
||||
spring.application.name=easyopen-service |
||||
|
||||
# nacos注册中心 |
||||
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 |
||||
|
||||
easyopen.prefix-path=/api |
@ -0,0 +1 @@ |
||||
spring.profiles.active=dev |
@ -0,0 +1,13 @@ |
||||
package com.gitee.sop.storyweb.controller.param; |
||||
|
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
|
||||
@Data |
||||
public class CategoryParam { |
||||
@ApiModelProperty(value = "分类名称", example = "娱乐") |
||||
private String categoryName; |
||||
|
||||
@ApiModelProperty(value = "分类故事") |
||||
private StoryParam storyParam; |
||||
} |
@ -0,0 +1,13 @@ |
||||
package com.gitee.sop.storyweb.controller.result; |
||||
|
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
|
||||
@Data |
||||
public class CategoryResult { |
||||
@ApiModelProperty(value = "分类名称", example = "娱乐") |
||||
private String categoryName; |
||||
|
||||
@ApiModelProperty(value = "分类故事") |
||||
private StoryResult storyResult; |
||||
} |
@ -0,0 +1,36 @@ |
||||
package com.gitee.sop.storyweb.controller.result; |
||||
|
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class FileUploadResult { |
||||
|
||||
private List<FileMeta> files = new ArrayList(); |
||||
|
||||
@Data |
||||
public static class FileMeta { |
||||
|
||||
public FileMeta(String filename, long size, String content) { |
||||
this.filename = filename; |
||||
this.size = size; |
||||
this.content = content; |
||||
} |
||||
|
||||
public FileMeta() { |
||||
} |
||||
|
||||
@ApiModelProperty(value = "文件名称", example = "1.txt") |
||||
private String filename; |
||||
@ApiModelProperty(value = "文件大小", example = "109") |
||||
private long size; |
||||
@ApiModelProperty(value = "文件内容", example = "啊啊啊") |
||||
private String content; |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
package com.gitee.sop.storyweb.controller.result; |
||||
|
||||
import io.swagger.annotations.ApiModelProperty; |
||||
import lombok.Data; |
||||
import org.hibernate.validator.constraints.Length; |
||||
|
||||
import javax.validation.constraints.NotBlank; |
||||
import java.util.Date; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class StoryResult { |
||||
@ApiModelProperty(value = "故事ID", example = "1") |
||||
private Long id; |
||||
|
||||
@ApiModelProperty(value = "故事名称", example = "海底小纵队") |
||||
private String name; |
||||
|
||||
@ApiModelProperty(value = "创建时间", example = "2019-04-14 19:02:12") |
||||
private Date gmt_create; |
||||
} |
@ -0,0 +1 @@ |
||||
file1 content,内容1 |
@ -0,0 +1 @@ |
||||
file2 content,内容2 |
@ -0,0 +1,29 @@ |
||||
HELP.md |
||||
/target/ |
||||
!.mvn/wrapper/maven-wrapper.jar |
||||
|
||||
### STS ### |
||||
.apt_generated |
||||
.classpath |
||||
.factorypath |
||||
.project |
||||
.settings |
||||
.springBeans |
||||
.sts4-cache |
||||
|
||||
### IntelliJ IDEA ### |
||||
.idea |
||||
*.iws |
||||
*.iml |
||||
*.ipr |
||||
|
||||
### NetBeans ### |
||||
/nbproject/private/ |
||||
/nbbuild/ |
||||
/dist/ |
||||
/nbdist/ |
||||
/.nb-gradle/ |
||||
/build/ |
||||
|
||||
### VS Code ### |
||||
.vscode/ |
@ -0,0 +1,13 @@ |
||||
package com.gitee.sop.websiteserver; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
||||
@SpringBootApplication |
||||
public class WebsiteServerApplication { |
||||
|
||||
public static void main(String[] args) { |
||||
SpringApplication.run(WebsiteServerApplication.class, args); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,17 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class DocInfo { |
||||
private String title; |
||||
@JSONField(serialize = false) |
||||
private String serviceId; |
||||
private List<DocModule> docModuleList; |
||||
} |
@ -0,0 +1,51 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class DocItem { |
||||
private String module; |
||||
private String name; |
||||
private String version; |
||||
private String summary; |
||||
private String description; |
||||
/** 是否多文件上传 */ |
||||
private boolean multiple; |
||||
/** http method列表 */ |
||||
private Collection<String> httpMethodList; |
||||
|
||||
private Collection<String> produces; |
||||
|
||||
private int moduleOrder; |
||||
|
||||
List<DocParameter> requestParameters; |
||||
List<DocParameter> responseParameters; |
||||
|
||||
public String getNameVersion() { |
||||
return name + version; |
||||
} |
||||
|
||||
/** |
||||
* 是否是上传文件请求 |
||||
* @return |
||||
*/ |
||||
public boolean isUploadRequest() { |
||||
boolean upload = false; |
||||
if (requestParameters != null) { |
||||
for (DocParameter requestParameter : requestParameters) { |
||||
String type = requestParameter.getType(); |
||||
if ("file".equalsIgnoreCase(type)) { |
||||
upload = true; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
return multiple || upload; |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class DocModule { |
||||
private String module; |
||||
private List<DocItem> docItems; |
||||
private int order; |
||||
} |
@ -0,0 +1,31 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField; |
||||
import lombok.Data; |
||||
import org.apache.commons.lang.StringUtils; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 参数 类型 是否必填 最大长度 描述 示例值 |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class DocParameter { |
||||
private String module; |
||||
private String name; |
||||
private String type; |
||||
private String maxLength = "-"; |
||||
private boolean required; |
||||
private String description; |
||||
private String example = ""; |
||||
|
||||
@JSONField(name = "x-example") |
||||
private String x_example = ""; |
||||
|
||||
private List<DocParameter> refs; |
||||
|
||||
public String getParamExample() { |
||||
return StringUtils.isBlank(example) ? x_example : example; |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import com.google.common.collect.Sets; |
||||
|
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
public class DocParserContext { |
||||
|
||||
/** |
||||
* 忽略显示 |
||||
*/ |
||||
public static volatile Set<String> ignoreHttpMethods = Sets.newHashSet("put", "options", "head", "put", "delete", "patch"); |
||||
} |
@ -0,0 +1,14 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class EurekaApplication { |
||||
private String name; |
||||
private List<EurekaInstance> instance; |
||||
} |
@ -0,0 +1,13 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class EurekaApplications { |
||||
private List<EurekaApplication> application; |
||||
} |
@ -0,0 +1,11 @@ |
||||
package com.gitee.sop.websiteserver.bean; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* @author tanghc |
||||
*/ |
||||
@Data |
||||
public class EurekaApps { |
||||
private EurekaApplications applications; |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue