No valid crumb was included in the request 问题定位与解决

背景

测试环境Jenkins版本进行了升级,发现代码中直接使用http调用Jenkins REST API的方法失效了,这边是除了GET以外其他的操作类的接口失效了,都是同样的错误,No valid crumb was included in the request。
No valid crumb was included in the request 问题定位与解决_第1张图片

百度的结果

搜索了具体的错误信息,发现都是一样的,就是说把Jenkins的CSRF给关闭了,但是我在Jenkins上面没有找到关闭CSRF的入口。就像下面这张图。
No valid crumb was included in the request 问题定位与解决_第2张图片
然后百度得知,Jenkins高版本关闭了页面上面取消CSRF防护的入口,本来也不推荐关闭。就想到失效的原因应该是之前的Jenkins一直没有打开CSRF防护,所以接口都是可以调用的。

解决

  • 方法一
    这边我当时是想,既然说没有这个crumb,那我就在请求的时候生成这个crumb头,然后加上再去调用接口不就好了。但是当我加上这个头之后,还是报同样的错误,很头疼。方法一GG。
    下面是我之前写的代码,大家也可以参考下。
    定义了一个crumb的实体对象。
// crumb头实体对象
@Data
private static class CrumbEntity implements Serializable {
    private String _class;
    private String crumb;
    private String crumbRequestField;
}

具体获取crumb头信息的方法。这边能够获取到crumb请求头,然后我也将这个头信息也加到了请求上,还是报错。

/**
 * 单独提取的一个公共方法
 *
 * @param url 请求地址
 * @param httpRequest 请求方法对象
 * @return
 */
private String customHttpMsg(String url, HttpRequest httpRequest) throws URISyntaxException, IOException {
    URI uri = new URI(url);
    HttpHost host = new HttpHost(uri.getHost(), uri.getPort());
    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
            new UsernamePasswordCredentials(JENKINS_USERNAME, JENKINS_PASSWORD));
    AuthCache authCache = new BasicAuthCache();
    BasicScheme basicScheme = new BasicScheme();
    authCache.put(host,basicScheme);
    try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build()) {
        HttpClientContext httpClientContext = HttpClientContext.create();
        httpClientContext.setAuthCache(authCache);
        CloseableHttpResponse response = httpClient.execute(host, httpRequest, httpClientContext);
        return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
    }
}

// 获取crumb头
private CrumbEntity getCrumb() throws IOException, URISyntaxException {
    String url = JENKINS_URL + "/crumbIssuer/api/json";
    HttpGet httpGet = new HttpGet(url);
    String res = customHttpMsg(url, httpGet);
    return JSON.parseObject(res, CrumbEntity.class);
}

// 测试获取crumb头
@Test
public void testGetCrumb() throws IOException, URISyntaxException {
    CrumbEntity crumb = getCrumb();
    log.info("crumb is {}", crumb);
}

// 测试方法调用前添加crumb头
// 但是测试无效
@Test
public void testAddCrumb() throws IOException, URISyntaxException {
    String url = JENKINS_URL+"/createItem/api/json?name="+JENKINS_PROJECT_NAME;
    HttpPost httpPost = new HttpPost(url);
    CrumbEntity crumb = getCrumb();
    // 添加crumb头
    httpPost.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb()));
    httpPost.setEntity(new StringEntity(JENKINS_PROJECT_XML, ContentType.create("text/xml", "utf-8")));
    String res = customHttpMsg(url, httpPost);
    log.info("res is {}", res);
}
  • 方法二
    接口调用不了了,很急,就想到这个日志应该是Jenkins打印出来的,然后就去下载了Jenkins的源码,全局搜索了这个错误日志,果然搜索到了,还有一段解释。
    这段代码在core/src/main/java/hudson/security/csrf/CrumbFilter.java里面,这个类是一个过滤器,方法中具体的逻辑是判断crumb头是否合法,然后再进行放行操作。
    No valid crumb was included in the request 问题定位与解决_第3张图片
    这边到具体抛出问题的位置,然后看了下具体的日志,日志里面是说使用这种token来代替,然后打开后面的链接。贴一下链接地址:https://jenkins.io/redirect/crumb-cannot-be-used-for-script
    No valid crumb was included in the request 问题定位与解决_第4张图片
    翻译大概是,CSRF的这个crumb头目前仅仅是对创建他们的Web会话有效,以限制攻击者的攻击。就是我们通过这个/crumbIssuer/api 获取到的crumb,除非是保存了Web会话信息,不然是无法调用CSRF保护的接口的。或者说使用API token,这种方式是不需要额外添加crumb头的。
    这么看,大概有两种方法可以解决这个问题。第一种是调用接口时,把会话信息带着,实现感觉有点复杂,没做。第二种,就是使用API token的方式,然后这个token怎么添加呢?

添加token

添加这个token很简单,我这边也简单整理了下。
选择Jenkins当前用户。选择配置,添加用户对应的一个token信息。
No valid crumb was included in the request 问题定位与解决_第5张图片
这块,生成的token需要自己保存下来,关于token的好处Jenkins也进行了说明,大家可以看看。我自己认为可能是为了安全吧,比如我们对外提供一个token而不是密码,当我们想要收回这个操作权限的时候,我们只需要把token删除掉就好了,而不需要去修改密码。
No valid crumb was included in the request 问题定位与解决_第6张图片
OK,添加完成token,然后再去调用请求,发现可以通过,这个问题也算解决了。

// 推荐使用token  
String JENKINS_TOKEN = "112c7c08043e02449e7fb97d253a657ede";

/**
 * 单独提取的一个公共方法
 *
 * @param url 请求地址
 * @param httpRequest 请求方法对象
 * @return
 */
private String customHttpMsg(String url, HttpRequest httpRequest) throws URISyntaxException, IOException {
    URI uri = new URI(url);
    HttpHost host = new HttpHost(uri.getHost(), uri.getPort());
    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
	// 注意这边  我把密码换成了token
    credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
            new UsernamePasswordCredentials(JENKINS_USERNAME, JENKINS_TOKEN));
    AuthCache authCache = new BasicAuthCache();
    BasicScheme basicScheme = new BasicScheme();
    authCache.put(host,basicScheme);
    try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build()) {
        HttpClientContext httpClientContext = HttpClientContext.create();
        httpClientContext.setAuthCache(authCache);
        CloseableHttpResponse response = httpClient.execute(host, httpRequest, httpClientContext);
        return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
    }
}

// 使用token 调用OK
@Test
public void testAddWithToken() throws IOException, URISyntaxException {
    // http://192.168.1.107:8081/jenkins/createItem/api/json?name=test
    String url = JENKINS_URL+"/createItem/api/json?name="+JENKINS_PROJECT_NAME;
    HttpPost httpPost = new HttpPost(url);
    httpPost.setEntity(new StringEntity(JENKINS_PROJECT_XML, ContentType.create("text/xml", "utf-8")));
    String res = customHttpMsg(url, httpPost);
    log.info("res is {}", res);
}

项目地址:https://github.com/yzh19961031/blogDemo/tree/master/jenkins_api
所有的代码都在代码仓上面,大家有兴趣或者有疑问可以去看看。

你可能感兴趣的:(Jenkins)