Jenkins插件开发篇
前言
想通过第三方平台直接能够单点登录到Jenkins,Jenkins本身是支持单点登录的,并且在Jenkins插件库中有提供相关的认证插件可以安装使用,但是并不能够完全适用,因为现有插件实现的单点是针对谷歌、GitLab等平台,想要让自研平台能够实现单点登录Jenkins,需要单独开发一个插件。
开发
想要开发Jenkins单点登录的认证插件,需要知道两个类,SecurityRealm、AbstractAuthenticationToken,SecurityRealm是用于进行认证操作,AbstractAuthenticationToken可以知道是用于保存认证token信息的。
在SecurityRealm中有两个比较重要的方法,我们通过继承这个类,然后在这个类里面实现这两个方法的业务逻辑,到时候在进行单点认证时Jenkins会调用这两个方法,下面简单说明下这两个方法的作用。
TestSecurityRealm.java
public class ITSMSecurityRealm extends SecurityRealm {
//这个方法是生成请求单点认证服务的code值地址,并重定向到这个地址,在请求授权码之前认证服务器会进行身份校验,只有过了认证服务器的身份认证之后才能够到重定向到授权码地址,所以可以在重定向前完成单点服务器的认证
public HttpResponse doCommenceLogin(StaplerRequest request, @Header("Referer") final String referer) throws IOException{
}
//这个方法是用于单点服务器回调地址,所以在单点认证服务器那边配置的回调地址需要配置为这个接口地址,然后通过request对象取url中的授权码,然后通过授权码去请求授权服务器颁布凭证,同理,通过授权码获取凭证的接口一样也需要过授权服务器的身份认证,所以可以在请求前先完成身份认证,获取到授权服务器的凭证后将用户认证信息保存在上下文中用于下次请求
public HttpResponse doFinishLogin(StaplerRequest request) throws IOException {
}
}
继承AbstractAuthenticationToken类用于存储请求授权服务器响应的凭证然后将其保存在Jenkins服务器上下文中。
安装插件
开发完业务逻辑后,将代码打包为.hpi后缀的插件,通过Jenkins页面方式进行安装开发的认证插件。
在Jenkins仪表盘找到系统管理
下拉找到插件管理
选择高级设置
下拉找到安装插件
选择打包好的hpi文件,点击部署,等待安装完成,然后重启Jenkins。
至此,插件安装完成。
配置
插件安装完成后还不会直接生效,需要配置单点登录的信息。
在仪表盘中找到系统管理,进入系统管理
下拉找到全局安全配置
安全域由Jenins自带数据库换成插件
然后下方会显示你插件中定义需要填写的参数信息,填写完后点击应用即可
配置完毕
补充
配置页面中的内容我们能够定义,只需要在resource目录下编写一个jelly的文件,然后在里面定义好需要填写的参数,但这个文件存放的路径是有说法的,路径需要对上SecurityRealm这个类的路径,不然是读取不到的。
读取这个配置参数的方法在SecurityRealm这个类中一个静态内部类的的方法。
在SecurityRealm这个类中定义个静态内部类实现Converter这个类,实现里面marshal方法实现配置文件读取。
public static final class ConverterImpl implements Converter {
@Override
public boolean canConvert(Class type) {
return type == ITSMSecurityRealm.class;
}
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
ITSMSecurityRealm realm = (ITSMSecurityRealm) source;
//这个"itsmUrl"需要与jelly文件中的属性名映射上
writer.startNode("itsmUrl");
writer.setValue(realm.getItsmUrl());
writer.endNode();
writer.startNode("clientID");
writer.setValue(realm.getClientID());
writer.endNode();
writer.startNode("secretClientSecret");
writer.setValue(realm.getSecretClientSecret().getEncryptedValue());
writer.endNode();
writer.startNode("callbackUrl");
writer.setValue(realm.getCallbackUrl());
writer.endNode();
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
String node = reader.getNodeName();
ITSMSecurityRealm realm = new ITSMSecurityRealm();
reader.moveDown();
node = reader.getNodeName();
String value = reader.getValue();
setValue(realm, node, value);
reader.moveUp();
reader.moveDown();
node = reader.getNodeName();
value = reader.getValue();
setValue(realm, node, value);
reader.moveUp();
reader.moveDown();
node = reader.getNodeName();
value = reader.getValue();
setValue(realm, node, value);
reader.moveUp();
reader.moveDown();
node = reader.getNodeName();
value = reader.getValue();
setValue(realm, node, value);
reader.moveUp();
if (reader.hasMoreChildren()) {
reader.moveDown();
node = reader.getNodeName();
value = reader.getValue();
setValue(realm, node, value);
reader.moveUp();
}
return realm;
}
private void setValue(ITSMSecurityRealm realm, String node, String value) {
if (node.equalsIgnoreCase("itsmUrl")) {
realm.setItsmUrl(value);
} else if (node.equalsIgnoreCase("clientid")) {
realm.setClientID(value);
} else if (node.equalsIgnoreCase("clientsecret")) {
realm.setClientSecret(value);
} else if (node.equalsIgnoreCase("secretclientsecret")) {
realm.setSecretClientSecret(Secret.fromString(value));
} else if (node.equalsIgnoreCase("callbackUrl")) {
realm.setCallbackUrl(value);
} else {
throw new ConversionException("invalid node value = " + node);
}
}
}
}
}
参考
相关案例在github上面有很多,本文只是大致解释其中逻辑,如果需要案例可以参考以下几个