结合系统鉴权的基于Swagger接口文档knife4j
前言
半年去前搭了一套结合业务系统鉴权的基于Swagger接口文档knife4j,用起来还不错,故而记录搭建。
痛点
如果没有使用类Swagger的接口文档框架,一般公司也会搭建一套独立的接口文档项目,比如之前使用的是CrapApi,一款完全开源的接口文档项目,哭诉一下这些独立接口文档项目的痛点。
① 编写费时费力
在没有使用接口文档框架之前的痛苦不言而喻,越是简单的curd写文档就越是痛苦,你会发现早上3小时优哉游哉写完接口,下午4小时键盘噼里啪啦写文档加测线上接口的现象。
② 即使写了也很难保证规范
一般都是写请求参数,一大堆乱七八糟的返回值就不管了,如果前端看不懂再问。
③ 迭代困难
新增接口一般会写文档,但是修改了接口经常就不管了,小小的改动还要改文档费时费力根本没法强制规范,导致后面干脆就不改动了,文档内容也就很陈旧,参考价值低。
④ 调试困难
这是个恶心后端,前端也不爱用的东西。恶心到了后端,文档自然质量不高,前端查看文档自然也不舒服,再加上一般接口文档项目调试功能薄弱,不可能像postman那么强大,所以前端更加不爱用,甚至还会复制接口出来,换到postman里重开一份文档测试。它无法做到类似postman统一域名前缀,缓存历史表单提交,请求头统一加token之类的便捷功能。
⑤ 查询困难
这个可能在高版本CrapApi文档项目里解决了,或者其它文档项目不存在这个问题。文档无法查找多级目录的接口内容,属实鸡肋,相当于根据url,接口备注之类查找接口文档的详细信息办不到。 终上所述,独立的接口文档编写就是个费力不讨好的事,除非严格的审查规范,否则产出鸡肋且低效。
Swagger的优势
■ 编写简单方便
平时写注释吗?答案是肯定的,那现在只是增加了几个注解,然后将注释移到注解里面。包括请求方式等等框架都会识别接口来帮助你生成,可见多么简便。
■ Swagger不会带来侵入性
Swagger是否带来侵入性,答案是否。很多反对使用Swagger的理由就是——对源码的侵入性太强了,导致代码臃肿,但是他们却认为javadoc(说白了就是根据规范的注释生成文档)很合理,这显然是偏见,swagger的注解其实可以看成是javadoc的注释,看明白注解后,注解其实就是分类,注解里的值便是非常规范的注释。人们都在提倡写好注释,而给注释包了一层规范后难道就要嫌弃它臃肿吗?
■ 迭代方便且版本可查
注解就在java项目里,每当有修改接口的时候,顺便修改一下注释(注解内容)是个顺手操作,如果这么简单的操作后端都不愿意修改,那他大概是在摸鱼准备跑路了。至于历史版本,跟着代码提交的自然存储于svn或git仓库里,查看历史也就非常简单方便。
■ 规范性
文档的规范性更不用怀疑,基于OpenAPI,规范的不能再规范了。
■ 调试方便
搭上Knife4j后,我甚至连postman都不爱用了,可见这套框架的优秀。该有的辅助功能都有,而且可以开启缓存,非常便捷。
■ 查询方便
根据control类生成层级,天然的优秀分类,关键字搜索也可透过层级查找达到过滤目的。
Swagger系列前世今生
注意是Swagger,不是湾湾的swag。
=> springdoc
Swagger1 => Swagger2 => Swagger3
=> swagger-bootstrap-ui => Knife4j
=> 其它系列ui框架
Swagger1(swagger-ui) => 一款RESTFUL接口的文档在线自动生成+功能测试功能框架,低版本引入麻烦。
Swagger2(springfox-swagger) => 默认的访问路径相较于swagger1不同,国内主流使用版本 swagger-ui/index.html
Swagger3(springfox-boot-starter) => springboot版,只需引用一个依赖,相对于Swagger2重新定义了一套注解,Swagger3将注解名称规范化,发布了一版然后就停更,烂尾了 swagger-ui.html
springdoc(springdoc-openapi-ui) => 基于swagger,swagger的ui很low,这里ui大变样,目前还在维护,可以当成一种ui重构的swagger
swagger-bootstrap-ui(com.github.xiaoymin) => 国产良心开源,Swagger的增强UI ,也有引用springdoc等源码,后期也支持更多的功能不仅仅是个ui框架
Knife4j => 改名,前身是swagger-bootstrap-ui,取名kni4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍!UI继续美化,且更适应微服务。
选型
■ 过程
引入框架自然是选新不选旧,直接上基于Swagger3的东西,springfox-boot-starter,一番折腾发现无法自动识别泛型返回值的内部字段且框架没有维护了。
那么回退版本至Swagger2的低版本,可以识别泛型返回值了,但是界面太丑了功能实在简陋简直鸡肋。
开始选型ui增强版的swagger底层框架,改用很火且还在维护的springdoc,搭好springdoc,界面果然强大起来,不过是基于Swagger3,还是无法识别泛型,弃用。这就难办了,选用Swagger2某些版本虽然可行,但是文档简陋,调试困难,谁用谁痛苦,而springdoc虽然功能强大,但是底层版本太新不能识别泛型无法契合实际的业务系统。
然后一番查找就发现了Knife4j这个框架,他的界面还要更加优秀,高版本仍然不识别泛型,经过一番调整,终于找到了合适的版本可以适用于当前业务系统。
最后多逛逛Knife4j社区,终于明白无法识别泛型的原因,然后升级Knife4j版本,可以体验最新的特性。
■ 最开始不用Swagger3的原因
不用Swagger3是为了识别泛型,那为什么一直强调要识别泛型呢?
我们一般会在接口返回值外面包一层通用的响应数据类,包含状态码,错误码,错误类型之类,如果成功业务请求,则会带上一个对象放在响应数据对象的data字段里,这个data对象的类一般是设置为Object类,才能通用。
如下接口和ResultDTO类
@ResponseBody
@RequestMapping(value = "api/ai", method = RequestMethod.POST)
public ResultDTO ai(@RequestBody Ai ai) {
try {
AiResponse aiResponse = aiServer.deal(ai);
return new ResultDTO(ResultDTO.SUCCESS_CODE, "成功返回", aiResponse);
} catch (Exception e) {
logger.error("ai报警出错", e);
return new ResultDTO(ResultDTO.ERROR_CODE, "参数出错", null);
}
}
ResultDTO
public class ResultDTO implements Serializable {
private static final long serialVersionUID = 1L;
public static final String ERROR_CODE = "-2";
public static final String SUCCESS_CODE = "0";
private String errcode;
private String errmsg;
private Object data;
public ResultDTO(String errcode, String errmsg, Object data) {
super();
this.errcode = errcode;
this.errmsg = errmsg;
this.data = data;
}
public static ResultDTO buildResult(String code, String msg) {
return new ResultDTO(code, msg, null);
}
public static ResultDTO buildResult(String code, String msg, Object data) {
return new ResultDTO(code, msg, data);
}
public String getErrcode() {
return errcode;
}
public void setErrcode(String errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
那么Object肯定是不能被swagger框架解析内部的字段的,这就需要改造成泛型,并且在接口中指定泛型,修改的代码不多即可支持泛型。
@ResponseBody
@RequestMapping(value = "api/ai", method = RequestMethod.POST)
@ApiOperation(value = "ai报警")
public ResultDTO<AiResponse> ai(@RequestBody Ai ai) {
try {
AiResponse aiResponse = aiServer.deal(ai);
return new ResultDTO(ResultDTO.SUCCESS_CODE, "成功返回", aiResponse);
} catch (Exception e) {
logger.error("ai报警出错", e);
return new ResultDTO(ResultDTO.ERROR_CODE, "参数出错", null);
}
}
再看下新的ResultDTO,data变成T了,swagger2低版本是可以自动向下解析T的内容,而swagger3和swagger2高版本不支持,这就会造成接口文档response里的data内容为空。
ResultDTO
@ApiModel(description = "响应数据封装")
public class ResultDTO<T> implements Serializable {
private static final long serialVersionUID = 1L;
public static final String ERROR_CODE = "-2";
public static final String SUCCESS_CODE = "0";
@ApiModelProperty(value = "响应状态码", position = 1)
private String errcode;
@ApiModelProperty(value = "响应信息", position = 2)
private String errmsg;
@ApiModelProperty(value = "响应数据", position = 3)
private T data;
public ResultDTO(String errcode, String errmsg, T data) {
super();
this.errcode = errcode;
this.errmsg = errmsg;
this.data = data;
}
public static<T> ResultDTO<T> buildResult(String code, String msg) {
return new ResultDTO(code, msg, null);
}
public static<T> ResultDTO<T> buildResult(String code, String msg, T data) {
return new ResultDTO(code, msg, data);
}
public String getErrcode() {
return errcode;
}
public void setErrcode(String errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
所以这类框架不是越新越好,适用才是王道,不用人云亦云,讲 swagger2 太low了,就跟风上 swagger3 ,结果发现各种bug,还不得乖乖回退回来,等待社区答案的出现和解决。
■ 泛型无法识别的原因
https://xiaoym.gitee.io/knife4j/faq/swagger-des-not-found.html
在2.0.6等后面的高版本中,由于升级了Springfox基础组件,如果开发者使用类似JRebel这类热加载插件的时候,会出现类字段没有的情况,目前没有办法解决springfox项目与JRebel插件的冲突,建议是不用JRebel
这才明白无法识别泛型bug是因为我本地用JRebel运行导致的,并不影响部署。解决方法是本地调试使用2.0.6以下版本方便调试,部署使用新版本以体验最新特性。或者用我的方案,后端选用2.0.5版本,前端可以用高版本。
搭建
■ pom
不断的踩坑明白了各版本的特性。3.X系列基于OpenAPI3,不过由于springfox停更,遗留蛮多未知的坑,knife4j作者也是不推荐使用。大版本持观望态度,小版本尽可能的更到最新,目前是2.0.9。2.0.6是一个转折点,多了一些不得不用的新特性,但是不兼容JRebel的bug也出来了。
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.6</version>
</dependency>
■ SwaggerConfig
SwaggerConfig 里有几处解释一下 ① @EnableSwagger2 是旧注解 @EnableSwagger2WebMvc 是2.0.6后的注解 ② AlternateTypeRule 定义的是几个系统类,存在互相循环嵌套的现象,例如A类有B类的对象,B类有A类的对象,会造成自动解析响应死循环。创建这个rule就是要排除解析这几个特殊类,实际上不多就几个。 ③ 只解析ApiOperation注解的接口是最靠谱的。 ④ 我最后后端选用2.0.5,大同小异,注解换一个,构造方法注释掉, 最后一句extensions注释掉即可。
import com.fasterxml.classmate.TypeResolver;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import persistence.Page;
import utils.StringUtils;
import sys.entity.Area;
import sys.entity.Office;
import sys.entity.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.AlternateTypeRule;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@EnableSwagger2WebMvc
@EnableKnife4j
@Configuration
public class SwaggerConfig {
private final OpenApiExtensionResolver openApiExtensionResolver;
@Autowired
public SwaggerConfig(OpenApiExtensionResolver openApiExtensionResolver) {
this.openApiExtensionResolver = openApiExtensionResolver;
}
@Bean
public Docket createRestApi() {
TypeResolver typeResolver = new TypeResolver();
AlternateTypeRule typeRule1 = new AlternateTypeRule(typeResolver.resolve(User.class),typeResolver.resolve(Object.class));
AlternateTypeRule typeRule2 = new AlternateTypeRule(typeResolver.resolve(Page.class),typeResolver.resolve(Object.class));
AlternateTypeRule typeRule3 = new AlternateTypeRule(typeResolver.resolve(Office.class),typeResolver.resolve(Object.class));
AlternateTypeRule typeRule4 = new AlternateTypeRule(typeResolver.resolve(Area.class),typeResolver.resolve(Object.class));
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("test")
.alternateTypeRules(typeRule1, typeRule2, typeRule3, typeRule4)
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths((s) -> {
if(StringUtils.contains(s, "api") || StringUtils.contains(s, "bbq")) {
return true;
}
return false;
})
.build()
.extensions(openApiExtensionResolver.buildExtensions("test"));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("服务接口文档")
.description("===========")
.contact(new Contact("bbq", "https://blog.csdn.net/qq_24054301?type=blog", "894944272@qq.com"))
.version("1.0")
.build();
}
}
■ 接口示例
@ApiImplicitParams({
@ApiImplicitParam(name = "classification", value = "统计单位", dataTypeClass = String.class, required = true),
@ApiImplicitParam(name = "type", value = "时间类型", dataTypeClass = String.class, required = true),
@ApiImplicitParam(name = "beginDate", value = "起始时间", dataTypeClass = String.class, required = false),
@ApiImplicitParam(name = "endDate", value = "结束时间", dataTypeClass = String.class, required = false),
@ApiImplicitParam(name = "isExportExcel", value = "报表excel导出 1:导出", dataTypeClass = String.class, required = false),
})
public ResultDTO<List<HandleStatisticLog>> statHandleAlarm(@ApiIgnore StatisticLog statisticLog,
@RequestParam(value = "isExportExcel", required = false, defaultValue = "") String isExportExcel,
HttpServletRequest request,
HttpServletResponse response) {
...........
return ResultDTO.buildResult(ResultDTO.SUCCESS_CODE, "", list);
}
@ApiModel(description = "报警统计日志")
public class StatisticLog extends DataEntity<StatisticLog> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("日期")
private Date date;
@ApiModelProperty("总数")
private Integer totalNum=0;
@ApiModelProperty("未处理数")
private Integer unHandleNum=0;
@ApiModelProperty("未审核数")
private Integer unCheckNum=0;
@ApiModelProperty("过期数")
....................
■ Properties
knife4j.authEnable=true
knife4j.enable=true
knife4j.setting.enableHost=true
knife4j.setting.enableHostText=http://doman:8080/proxyApi
效果一览
主页
根据control类归类
请求参数
响应
这里可以看到泛型的内容识别到了
请求参数
会自动生成一个json例子,注释在后面。
前端乱码报错
2个条件 win下、jar包里放js,请求会出现乱码情况,被强转为anni。
如果在win下使用java -jar(没有-dfile.encoding=utf-8)的方式跑 ui2.0.5版本的js会乱码(utf-8内容被转成gbk)包括postman请求。在3.0.2版本中,这点好似被处理了,其它方式请求正常utf-8,但是较新的谷歌浏览器请求仍然是乱码(utf-8内容被转成gbk) 类似这个提问 https://www.oschina.net/question/3639571_2322823 我最后采用的nginx跑静态文件,否则需要强制设置编码utf-8,在此做个记录。 https://www.oschina.net/question/3639571_2322823 https://gitee.com/xiaoym/knife4j/issues/I3Y37F
所以本地调试可以用maven里自带的前端,部署上线最好使用nginx跑静态文件。
统一默认请求前缀
在2.0.6版本及以上,可在后端配置字段设置,由于2.0.6与JRebel的冲突,后端使用了2.0.5版本,所以没有这个功能。不过我们可以直接自己编译前端来达到这个目的,我选用的是2.0.6版本的前端项目knife4j-vue,没有冲突,并且拥有afterScript功能挺有意思(https://doc.xiaominfo.com/knife4j/documentation/afterScript.html)。 源码找到 constants.js 50行有defaultSettings
defaultSettings: {
enableSwaggerModels:true,
enableDocumentManage:true,
showApiUrl: false,
showTagStatus: false,
enableSwaggerBootstrapUi: false,
treeExplain: true,
enableDynamicParameter: true,
enableFilterMultipartApis: false,
enableFilterMultipartApiMethodType: "POST",
enableRequestCache: true,
enableCacheOpenApiTable: false,
enableHost:true,
enableHostText:"http://doman/proxyApi",
language: "zh-CN"
},
然后重新用yarn编译即可。
权限认证方案
如果搭建到线上必定需要一套鉴权系统。 查看 knife4j 文档可以看到这个,
3.5.2 访问页面加权控制 https://doc.xiaominfo.com/knife4j/documentation/accessControl.html#_3-5-2-%E8%AE%BF%E9%97%AE%E9%A1%B5%E9%9D%A2%E5%8A%A0%E6%9D%83%E6%8E%A7%E5%88%B6 不管是官方的swagger-ui.html或者doc.html,目前接口访问都是无需权限即可访问接口文档的,很多朋友以前问我能不能提供一个登陆界面的功能,开发者输入用户名和密码来控制界面的访问,只有知道用户名和密码的人才能访问此文档 做登录页控制需要有用户的概念,所以相当长一段时间都没有提供此功能 针对Swagger的资源接口,Knife4j提供了简单的Basic认证功能
自带的是简单的basic,防君子不防小人。我有两种方案,一种是通过nginx来控制ip访问白名单,再一种是结合业务系统的token来认证鉴权,我使用第二种。
方案一 Nginx动态ip白名单
① geo 模块放白名单配置
geo 模块和 server 同级,ip配置到独立的conf的文件里
geo $remote_addr $passiplist {
default 0;
include .../conf/companyIp.conf;
include .../conf/whiteIp.conf;
}
② 代理中加判断拦截
server 里需要拦截的代理添加这句
if ( $passiplist = 0 ) {
return 403;
}
③ 设置公司开发环境ip白名单
公司开发环境一定是要实时保证白名单的,如果公司ip无法固定,可以创建一个接口不断更新ip 接口缓存请求来源的ip到redis
@ResponseBody
@RequestMapping(value = ".../setCompanyIp", method = RequestMethod.POST)
public String setCompanyIp(String auth, HttpServletRequest request) {
try {
if(auth != null && auth.equals(Global.getConfig("nginx.ip.auth"))) {
return JedisUtils.set("ip:company", StringUtils.getRemoteAddr(request), 0);
}
return "no auth";
} catch (Exception e) {
logger.error("设置公司ip失败", e);
return "设置公司ip失败";
}
}
bat请求设置公司开发环境ip的接口,并创建定时任务轮询。
rem ****** 设置公司ip ********
@echo off
C:/.../curl.exe -X POST -d "auth=..." https://.../setCompanyIp -k
@echo on
④ 设置白名单ip
非开发环境外的ip可以使用另外一个接口设置白名单。 这个接口要保证安全调用,不可外泄,这里仅用类basic认证。
@ResponseBody
@RequestMapping(value = ".../addWhiteIp", method = RequestMethod.POST)
public String addWhiteIp(String auth, HttpServletRequest request) {
Jedis jedis = null;
try {
if(auth != null && auth.equals(Global.getConfig("nginx.ip.auth"))) {
String ip = StringUtils.getRemoteAddr(request);
jedis = JedisUtils.getResource();
jedis.hset("ip:white", ip, "" + System.currentTimeMillis());
return "success";
}
return "no auth";
} catch (Exception e) {
logger.error("设置白名单ip失败", e);
return "设置白名单ip失败";
} finally {
JedisUtils.returnResource(jedis);
}
}
⑤ 获取白名单ip
两个接口返回白名单ip配置字符串,一个固定是公司ip,一个是ip列表 并且更新ip列表的缓存数据,例如超过2个小时的剔除。
@ResponseBody
@RequestMapping(value = ".../getCompanyIp", method = RequestMethod.GET)
public String getCompanyIp() {
try {
String ip = JedisUtils.get("ip:company");
ip = StringUtils.isNotBlank(ip) ? ip + " 1;" : "";
return ip;
} catch (Exception e) {
logger.error("失败获取白名单ip", e);
return "";
}
}
@ResponseBody
@RequestMapping(value = ".../getWhiteIp", method = RequestMethod.GET)
public String getWhiteIp() {
Jedis jedis = null;
try {
jedis = JedisUtils.getResource();
Map<String, String> map = jedis.hgetAll("ip:white");
Iterator iter = map.entrySet().iterator();
String ip = "";
while (iter.hasNext()){
Map.Entry e = (Map.Entry) iter.next();
if(System.currentTimeMillis() - Long.parseLong((String) e.getValue()) > 21600000) {
jedis.hdel("ip:white",(String) e.getKey());
} else {
ip += e.getKey() + " 1;\n";
}
}
return ip;
} catch (Exception e) {
logger.error("获取白名单ip失败", e);
return "";
} finally {
JedisUtils.returnResource(jedis);
}
}
⑥ 服务端写入 bat为例,获取ip配置写入到临时文件,再和ip配置文件比对,如果有不同就替换文件并优雅重启nginx 2个bat类似,然后定时任务轮询即可。
rem ****** 获取公司ip ********
@echo off
...\curl.exe -X GET https://.../getCompanyIp -k > ...\companyIp_tmp.conf
fc ...\companyIp.conf ...\companyIp_tmp.conf >nul
if errorlevel 1 (
echo 文件变动,覆盖中。。。
echo F | xcopy /y "...\companyIp_tmp.conf" "...\companyIp.conf"
del ...\companyIp_tmp.conf
rem 延迟3s
ping -n 3 1271
d:
cd ...\nginx
nginx -s reload
rem pause
exit
)
del ...\companyIp_tmp.conf
echo 文件无变动,无操作。。。
rem pause
@echo on
方案二 结合业务系统的token
① Swagger相关接口设置访问权限
首先修改shiro配置,设置需要api:doc:use权限标识符才能访问。
@Bean
public String shiroFilterChainDefinitions() {
StringBuffer shiroFilterChainDefinitions = new StringBuffer();
............
shiroFilterChainDefinitions.append(adminPath + "/**/noanth/** = anon \n");
shiroFilterChainDefinitions.append(adminPath + "/**/api/** = apifilter \n");
shiroFilterChainDefinitions.append(adminPath + "/**/internalauth/** = internalfilter \n");
shiroFilterChainDefinitions.append(adminPath + "/login = authc \n");
shiroFilterChainDefinitions.append(adminPath + "/** = user \n");
if(knife4jEnable) {
shiroFilterChainDefinitions.append("/swagger-resources/** = apifilter,perms[api:doc:use] \n");
shiroFilterChainDefinitions.append("/v2/api-docs = apifilter,perms[api:doc:use] \n");
}
return shiroFilterChainDefinitions.toString();
}
② Nginx静态文件设置访问权限
两个接口代理到java鉴权,vue的js等静态没做鉴权,doc.html可以做一个鉴权,使用internal模块。 nginx内部鉴权可参考 视频直播鉴权结合业务系统的token或session https://blog.csdn.net/qq_24054301/article/details/119247801
location /internalfilter {
internal;
proxy_set_header Host $host;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
if ( $request_uri ~ ((\?)([\S\s]*)) ){
set $parameter $1;
}
proxy_pass http://doman/.../internalauth/advanced/api:doc:use$parameter;
}
location /doc.html {
auth_request /internalfilter;
error_page 401 = @error401;
error_page 403 = @error403;
alias ...knife4j\\doc.html;
}
location /webjars{
alias ...knife4j\\webjars;
}
location /swagger-resources/{
proxy_pass http://doman/swagger-resources/;
}
location /v2/api-docs{
proxy_pass http://doman/v2/api-docs;
}
location @error401 {
return 401 $query_string"没有登录";
}
location @error403 {
return 403 "没有权限访问";
}
③ token传参方案
上面该鉴权的都拦截了,下一步就是如何让knife4j前端请求时带上token。 方法一 利用同源cookie 这是我目前在用的,灰常好用,因为knife4j前端部署和pc端业务系统前端部署在同源下,请求任何接口都会自动带上pc端业务系统cookie,也就是说这个knife4j成了pc端业务系统的一模块而已,那边登陆了这边也就登陆。
如图,我业务系统登录了拥有权限的号,在列表中点击第一条既可弹出接口文档,由于带有cookie,也就访问成功。 退出登录再切换为没有操作权限的账号,再刷新doc.html,马上跳转到没有权限访问。 并且值得一提的是,在knife4j接口文档里面调试业务接口的时候,也会自动带上cookie,也就是鉴权进入文档后不需要添加全局token就可调试,灰常方便且安全,需要调试其它账号时,再传递token就可以覆盖。(参数token优先级大于请求头token大于cookie中的token,这个优先级是shiro那边设置的)
方法二 将第三方页面嵌入iframe
一个带有iframe的html,从cookie或者url或者请求头获取token,再统一添加XMLHttpRequest请求头token 访问 http://doman/setToken.html?url=http://doman/doc.html
setToken.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XHR请求头统一增加token</title>
<style>
html,body{
height:100%;
}
</style>
<script src="//domain/jquery-1.9.1.min.js"></script>
</head>
<body >
<iframe id='iframe' width="100%" height="100%" frameborder="0"></iframe>
</body>
<script>
var url = getQueryVariable("url")
var token = getCookie("token")
var _send = window.XMLHttpRequest.prototype.send
window.XMLHttpRequest.prototype.send = function() {
this.setRequestHeader('token', token);
return _send.apply(this, arguments)
}
var bbq=document.getElementById("iframe");
bbq.contentWindow.document.write("<script>var _send = window.XMLHttpRequest.prototype.send;window.XMLHttpRequest.prototype.send = function(){this.setRequestHeader('token', '" + token + "');return _send.apply(this, arguments);}<\/script>")
$.ajax({
url: url,
success: function (data) {
var iframeDoc = bbq.contentWindow.document;
iframeDoc.open('text/html', 'replace');
iframeDoc.write(data);
iframeDoc.close();
document.title=iframeDoc.title;
}
})
function getCookie(name) {
var strCookie = document.cookie;
var arrCookie = strCookie.split(';');
for(var i = 0; i < arrCookie.length; i++) {
var arr = arrCookie[i].split('=');
if(arr[0].replace(/(^\s*)|(\s*$)/g, "") == name) {
return arr[1];
}
}
return '';
}
function getQueryVariable(variable){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
</script>
</html>
方法三 下载源码修改 下载源码定制化开发,费时费力些,不过也是个方案。
最后
这套系统已经相当稳定了,并且国内有knife4j的作者这样的大佬支持,经常抽出时间解决issues,不求回报,非常值得尝试使用。
|