feat(系统设置): ldap测试连接,测试登录接口

This commit is contained in:
WangXu10 2023-08-10 17:53:50 +08:00 committed by 刘瑞斌
parent c4b5d2dd55
commit 7e2015a13e
10 changed files with 464 additions and 2 deletions

View File

@ -0,0 +1,84 @@
package io.metersphere.sdk.ldap;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class CustomSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory socketFactory;
public CustomSSLSocketFactory() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new DummyTrustmanager()}, new SecureRandom());
socketFactory = ctx.getSocketFactory();
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
public static SocketFactory getDefault() {
return new CustomSSLSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return socketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return socketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException {
return socketFactory.createSocket(socket, string, num, bool);
}
@Override
public Socket createSocket(String string, int num) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num);
}
@Override
public Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num, netAdd, i);
}
@Override
public Socket createSocket(InetAddress netAdd, int num) throws IOException {
return socketFactory.createSocket(netAdd, num);
}
@Override
public Socket createSocket(InetAddress netAdd1, int num, InetAddress netAdd2, int i) throws IOException {
return socketFactory.createSocket(netAdd1, num, netAdd2, i);
}
/**
* 证书
*/
public static class DummyTrustmanager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.sdk.ldap;
import org.springframework.ldap.core.support.LdapContextSource;
import javax.naming.Context;
import java.util.Hashtable;
public class SSLLdapContextSource extends LdapContextSource {
public Hashtable<String, Object> getAnonymousEnv() {
Hashtable<String, Object> anonymousEnv = super.getAnonymousEnv();
anonymousEnv.put("java.naming.security.protocol", "ssl");
anonymousEnv.put("java.naming.ldap.factory.socket", CustomSSLSocketFactory.class.getName());
anonymousEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
return anonymousEnv;
}
}

View File

@ -0,0 +1,162 @@
package io.metersphere.sdk.ldap.service;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.ldap.SSLLdapContextSource;
import io.metersphere.sdk.ldap.vo.LdapLoginRequest;
import io.metersphere.sdk.ldap.vo.LdapRequest;
import io.metersphere.sdk.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.InvalidNameException;
import org.springframework.ldap.InvalidSearchFilterException;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.query.SearchScope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.naming.directory.DirContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
@Service
@Transactional(rollbackFor = Exception.class)
public class LdapService {
public void testConnect(LdapRequest request) {
getConnect(request);
}
private LdapTemplate getConnect(LdapRequest request) {
String credentials = EncryptUtils.aesDecrypt(request.getLadpPassword()).toString();
LdapContextSource sourceLdapCtx;
if (StringUtils.startsWithIgnoreCase(request.getLdapUrl(), "ldaps://")) {
sourceLdapCtx = new SSLLdapContextSource();
// todo 这里加上strategy 会报错
} else {
sourceLdapCtx = new LdapContextSource();
}
sourceLdapCtx.setUrl(request.getLdapUrl());
sourceLdapCtx.setUserDn(request.getLadpDn());
sourceLdapCtx.setPassword(credentials);
sourceLdapCtx.setDirObjectFactory(DefaultDirObjectFactory.class);
sourceLdapCtx.afterPropertiesSet();
LdapTemplate ldapTemplate = new LdapTemplate(sourceLdapCtx);
ldapTemplate.setIgnorePartialResultException(true);
Map<String, Object> baseEnv = new Hashtable<>();
baseEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");
baseEnv.put("com.sun.jndi.ldap.read.timeout", "3000");
sourceLdapCtx.setBaseEnvironmentProperties(baseEnv);
ldapTemplate.setDefaultSearchScope(SearchScope.SUBTREE.getId());
try {
authenticate(request.getLadpDn(), credentials, ldapTemplate);
} catch (AuthenticationException e) {
LogUtils.error(e.getMessage(), e);
throw new MSException(Translator.get("ldap_connect_fail_user"));
} catch (Exception e) {
LogUtils.error(e.getMessage(), e);
throw new MSException(Translator.get("ldap_connect_fail"));
}
return ldapTemplate;
}
private boolean authenticate(String dn, String credentials, LdapTemplate ldapTemplate) throws AuthenticationException {
DirContext ctx = null;
try {
ctx = ldapTemplate.getContextSource().getContext(dn, credentials);
return true;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext((LdapContext) ctx);
}
}
/**
* 测试登录
*
* @param request
* @return
*/
public DirContextOperations testLogin(LdapLoginRequest request) {
String credentials = request.getPassword();
DirContextOperations dirContextOperations = null;
try {
LdapTemplate ldapTemplate = getLdapTemplate(request);
// 获取LDAP用户相关信息
dirContextOperations = getContextMapper(request, ldapTemplate);
// 执行登录认证
authenticate(String.valueOf(dirContextOperations.getDn()), credentials, ldapTemplate);
} catch (AuthenticationException e) {
LogUtils.error(e.getMessage(), e);
throw new MSException(Translator.get("authentication_failed"));
}
// 检查属性是否存在
getMappingAttr("name", dirContextOperations, request);
return dirContextOperations;
}
private LdapTemplate getLdapTemplate(LdapLoginRequest request) {
LdapRequest ldapRequest = new LdapRequest();
BeanUtils.copyBean(ldapRequest, request);
LdapTemplate ldapTemplate = getConnect(ldapRequest);
return ldapTemplate;
}
public String getMappingAttr(String attr, DirContextOperations dirContext, LdapLoginRequest request) {
// 检查LDAP映射属性
String mapping = request.getLdapUserMapping();
Map jsonObject = JSON.parseObject(mapping, Map.class);
String mapAttr = (String) jsonObject.get(attr);
String result = dirContext.getStringAttribute(mapAttr);
return result;
}
public DirContextOperations getContextMapper(LdapLoginRequest request, LdapTemplate ldapTemplate) {
String filter = request.getLdapUserFilter();
String[] arr = request.getLdapUserOu().split("|");
List<DirContextOperations> result = null;
// 多OU
for (String ou : arr) {
try {
result = ldapTemplate.search(query().base(ou.trim()).filter(filter, request.getUsername()), new MsContextMapper());
if (result.size() == 1) {
return result.get(0);
}
} catch (NameNotFoundException | InvalidNameException e) {
LogUtils.error(e.getMessage(), e);
throw new MSException(Translator.get("login_fail_ou_error"));
} catch (InvalidSearchFilterException e) {
LogUtils.error(e.getMessage(), e);
throw new MSException(Translator.get("login_fail_filter_error"));
}
}
if (result.size() != 1) {
throw new MSException(Translator.get("user_not_found_or_not_unique"));
}
return result.get(0);
}
private static class MsContextMapper extends AbstractContextMapper<DirContextOperations> {
@Override
public DirContextOperations doMapFromContext(DirContextOperations context) {
return context;
}
}
}

View File

@ -0,0 +1,40 @@
package io.metersphere.sdk.ldap.vo;
import io.metersphere.sdk.dto.LoginRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class LdapLoginRequest extends LoginRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "LDAP地址", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_url_is_null}")
private String ldapUrl;
@Schema(description = "LDAP绑定DN", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_dn_is_null}")
private String ldapDn;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_password_is_null}")
private String ldapPassword;
@Schema(description = "用户过滤器", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_user_filter_is_null}")
private String ldapUserFilter;
@Schema(description = "用户OU", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_ou_is_null}")
private String ldapUserOu;
@Schema(description = "LDAP属性映射", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_user_mapping_is_null}")
private String ldapUserMapping;
}

View File

@ -0,0 +1,26 @@
package io.metersphere.sdk.ldap.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
public class LdapRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "LDAP地址", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_url_is_null}")
private String ldapUrl;
@Schema(description = "LDAP绑定DN", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_dn_is_null}")
private String ladpDn;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{ldap_password_is_null}")
private String ladpPassword;
}

View File

@ -4,6 +4,9 @@ import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.dto.BasePageRequest;
import io.metersphere.sdk.ldap.service.LdapService;
import io.metersphere.sdk.ldap.vo.LdapLoginRequest;
import io.metersphere.sdk.ldap.vo.LdapRequest;
import io.metersphere.sdk.log.annotation.Log;
import io.metersphere.sdk.log.constants.OperationLogType;
import io.metersphere.sdk.util.PageUtils;
@ -17,6 +20,7 @@ import io.metersphere.system.service.AuthSourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
@ -27,10 +31,14 @@ import java.util.List;
@Tag(name = "认证设置")
@RestController
@RequestMapping("/system/authsource")
@Data
public class AuthSourceController {
@Resource
private AuthSourceService authSourceService;
@Resource
private LdapService ldapService;
@PostMapping("/list")
@Operation(summary = "认证设置列表查询")
@RequiresPermissions(PermissionConstants.SYSTEM_PARAMETER_SETTING_AUTH_READ)
@ -76,7 +84,21 @@ public class AuthSourceController {
@Operation(summary = "更新状态")
@RequiresPermissions(PermissionConstants.SYSTEM_PARAMETER_SETTING_AUTH_READ_UPDATE)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.updateLog(#request.getId())", msClass = AuthSourceLogService.class)
public AuthSource updateStatus(@Validated @RequestBody AuthSourceStatusRequest request ) {
public AuthSource updateStatus(@Validated @RequestBody AuthSourceStatusRequest request) {
return authSourceService.updateStatus(request.getId(), request.getEnable());
}
@PostMapping("/ldap/test-connect")
@Operation(summary = "ladp测试连接")
@RequiresPermissions(PermissionConstants.SYSTEM_PARAMETER_SETTING_AUTH_READ_UPDATE)
public void ldapTestConnect(@Validated @RequestBody LdapRequest request) {
ldapService.testConnect(request);
}
@PostMapping("/ldap/test-login")
@RequiresPermissions(PermissionConstants.SYSTEM_PARAMETER_SETTING_AUTH_READ_UPDATE)
public void testLogin(@RequestBody LdapLoginRequest request) {
ldapService.testLogin(request);
}
}

View File

@ -8,6 +8,7 @@ import io.metersphere.system.domain.SystemParameter;
import io.metersphere.system.mapper.SystemParameterMapper;
import jakarta.annotation.Resource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -62,6 +63,11 @@ public class BaseDisplayService {
break;
}
}
String[] split = systemParameter.getParamValue().split("[.\n]");
if (StringUtils.equalsAnyIgnoreCase("svg", split[split.length - 1])) {
contentType = MediaType.valueOf("image/svg+xml");
}
return ResponseEntity.ok()
.contentType(contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")

View File

@ -5,18 +5,26 @@ import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.dto.BasePageRequest;
import io.metersphere.sdk.ldap.service.LdapService;
import io.metersphere.sdk.ldap.vo.LdapLoginRequest;
import io.metersphere.sdk.ldap.vo.LdapRequest;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Pager;
import io.metersphere.system.domain.AuthSource;
import io.metersphere.system.request.AuthSourceRequest;
import io.metersphere.system.request.AuthSourceStatusRequest;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@ -50,13 +58,22 @@ public class AuthSourceControllerTests extends BaseTest {
private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError();
public static final String LDAP_TEST_CONNECT = "/system/authsource/ldap/test-connect";
public static final String LDAP_TEST_LOGIN = "/system/authsource/ldap/test-login";
@Mock
private LdapService ldapService;
@Resource
AuthSourceController authSourceController;
@Test
@Order(1)
public void testAddSource() throws Exception {
AuthSourceRequest authSource = new AuthSourceRequest();
authSource.setName("测试CAS");
authSource.setType("CAS");
this.requestPost(AUTH_SOURCE_ADD, authSource,ERROR_REQUEST_MATCHER);
this.requestPost(AUTH_SOURCE_ADD, authSource, ERROR_REQUEST_MATCHER);
authSource.setConfiguration("123");
this.requestPost(AUTH_SOURCE_ADD, authSource);
@ -180,4 +197,46 @@ public class AuthSourceControllerTests extends BaseTest {
.andExpect(resultMatcher).andDo(print())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
@Test
@Order(11)
public void testLdapConnectMock() throws Exception {
authSourceController.setLdapService(ldapService);
LdapRequest ldapRequest = getRequest("ldaps://127.1.1.1", "cn=admin,dc=example,dc=org", "admin");
Mockito.doNothing().when(ldapService).testConnect(ldapRequest);
this.requestPostAndReturn(LDAP_TEST_CONNECT, ldapRequest);
}
@Test
@Order(12)
public void testLdapLoginMock() throws Exception {
authSourceController.setLdapService(ldapService);
LdapLoginRequest loginRequest = getLoginRequest("ldap://127.1.1.1", "cn=admin,dc=example,dc=org", "admin", "cn=admin,dc=example,dc=org", "(|(uid={0})(mail={0}))", "{\"username\":\"uid\",\"name\":\"cn\",\"email\":\"mail\"}", "admin", "admin");
DirContextOperations operations = new DirContextAdapter();
Mockito.when(ldapService.testLogin(loginRequest)).thenReturn(operations);
this.requestPostAndReturn(LDAP_TEST_LOGIN, loginRequest);
}
private LdapLoginRequest getLoginRequest(String ldapUrl, String ldapDn, String ldapPassword, String ldapUserOu, String ldapUserFilter, String ldapUserMapping, String username, String password) {
LdapLoginRequest loginRequest = new LdapLoginRequest();
loginRequest.setLdapUrl(ldapUrl);
loginRequest.setLdapDn(ldapDn);
loginRequest.setLdapPassword(ldapPassword);
loginRequest.setLdapUserOu(ldapUserOu);
loginRequest.setLdapUserFilter(ldapUserFilter);
loginRequest.setLdapUserMapping(ldapUserMapping);
loginRequest.setUsername(username);
loginRequest.setPassword(password);
return loginRequest;
}
private LdapRequest getRequest(String ldapUrl, String ldapDn, String ldapPassword) {
LdapRequest ldapRequest = new LdapRequest();
ldapRequest.setLdapUrl(ldapUrl);
ldapRequest.setLadpDn(ldapDn);
ldapRequest.setLadpPassword(ldapPassword);
return ldapRequest;
}
}

View File

@ -0,0 +1,28 @@
package io.metersphere.system.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LdapLoginRequestDefinition {
@NotBlank(message = "{ldap_url_is_null}")
private String ldapUrl;
@NotBlank(message = "{ldap_dn_is_null}")
private String ldapDn;
@NotBlank(message = "{ldap_password_is_null}")
private String ldapPassword;
@NotBlank(message = "{ldap_user_filter_is_null}")
private String ldapUserFilter;
@NotBlank(message = "{ldap_ou_is_null}")
private String ldapUserOu;
@NotBlank(message = "{ldap_user_mapping_is_null}")
private String ldapUserMapping;
}

View File

@ -0,0 +1,19 @@
package io.metersphere.system.controller.param;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LdapRequestDefinition {
@NotBlank(message = "{ldap_url_is_null}")
private String ldapUrl;
@NotBlank(message = "{ldap_dn_is_null}")
private String ladpDn;
@NotBlank(message = "{ldap_password_is_null}")
private String ladpPassword;
}