diff --git a/README.md b/README.md index 23ae3b68dc..43aa6b5e25 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # MeterSphere 开源持续测试平台 +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/metersphere/metersphere)](https://github.com/metersphere/metersphere/releases/latest) +[![GitHub All Releases](https://img.shields.io/github/downloads/metersphere/metersphere/total)](https://github.com/metersphere/metersphere/releases) + MeterSphere 是一站式的开源企业级持续测试平台,涵盖测试跟踪、接口测试、性能测试、团队协作等功能,兼容JMeter 等开源标准,有效助力开发和测试团队充分利用云弹性进行高度可扩展的自动化测试,加速高质量软件的交付。 - 测试跟踪: 远超 TestLink 的使用体验; @@ -17,7 +20,7 @@ UI 展示: 仅需两步快速安装 MeterSphere: 1. 准备一台不小于 8 G内存的 64位 Linux 主机; - 2. 执行如下命令一键安装 MeterSphere。 + 2. 以 root 用户执行如下命令一键安装 MeterSphere。 ```sh curl -sSL https://github.com/metersphere/metersphere/releases/latest/download/quick_start.sh | sh diff --git a/backend/pom.xml b/backend/pom.xml index c2f9723783..500ac3be5a 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -161,6 +161,12 @@ 2.3.0 + + + org.springframework.boot + spring-boot-starter-data-ldap + + diff --git a/backend/src/main/java/io/metersphere/config/ShiroConfig.java b/backend/src/main/java/io/metersphere/config/ShiroConfig.java index ac7b6ab17d..0bad942db0 100644 --- a/backend/src/main/java/io/metersphere/config/ShiroConfig.java +++ b/backend/src/main/java/io/metersphere/config/ShiroConfig.java @@ -44,6 +44,7 @@ public class ShiroConfig { filterChainDefinitionMap.put("/resource/**", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/signin", "anon"); + filterChainDefinitionMap.put("/ldap/signin", "anon"); filterChainDefinitionMap.put("/isLogin", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); diff --git a/backend/src/main/java/io/metersphere/controller/LoginController.java b/backend/src/main/java/io/metersphere/controller/LoginController.java index 0097768a5b..cc339067bb 100644 --- a/backend/src/main/java/io/metersphere/controller/LoginController.java +++ b/backend/src/main/java/io/metersphere/controller/LoginController.java @@ -36,52 +36,7 @@ public class LoginController { @PostMapping(value = "/signin") public ResultHolder login(@RequestBody LoginRequest request) { - String msg; - String username = StringUtils.trim(request.getUsername()); - String password = StringUtils.trim(request.getPassword()); - if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { - return ResultHolder.error("user or password can't be null"); - } - - UsernamePasswordToken token = new UsernamePasswordToken(username, password); - Subject subject = SecurityUtils.getSubject(); - - try { - subject.login(token); - if (subject.isAuthenticated()) { - UserDTO user = (UserDTO) subject.getSession().getAttribute(ATTR_USER); - // 自动选中组织,工作空间 - if (StringUtils.isEmpty(user.getLastOrganizationId())) { - List userRoles = user.getUserRoles(); - List test = userRoles.stream().filter(ur -> ur.getRoleId().startsWith("test")).collect(Collectors.toList()); - List org = userRoles.stream().filter(ur -> ur.getRoleId().startsWith("org")).collect(Collectors.toList()); - if (test.size() > 0) { - String wsId = test.get(0).getSourceId(); - userService.switchUserRole("workspace", wsId); - } else if (org.size() > 0) { - String orgId = org.get(0).getSourceId(); - userService.switchUserRole("organization", orgId); - } - } - // 返回 userDTO - return ResultHolder.success(subject.getSession().getAttribute("user")); - } else { - return ResultHolder.error(Translator.get("login_fail")); - } - } catch (ExcessiveAttemptsException e) { - msg = Translator.get("excessive_attempts"); - } catch (LockedAccountException e) { - msg = Translator.get("user_locked"); - } catch (DisabledAccountException e) { - msg = Translator.get("user_has_been_disabled"); - } catch (ExpiredCredentialsException e) { - msg = Translator.get("user_expires"); - } catch (AuthenticationException e) { - msg = e.getMessage(); - } catch (UnauthorizedException e) { - msg = Translator.get("not_authorized") + e.getMessage(); - } - return ResultHolder.error(msg); + return userService.login(request); } @GetMapping(value = "/signout") diff --git a/backend/src/main/java/io/metersphere/ldap/LdapService.java b/backend/src/main/java/io/metersphere/ldap/LdapService.java new file mode 100644 index 0000000000..b6b5b6d798 --- /dev/null +++ b/backend/src/main/java/io/metersphere/ldap/LdapService.java @@ -0,0 +1,57 @@ +package io.metersphere.ldap; + +import io.metersphere.commons.exception.MSException; +import io.metersphere.controller.request.LoginRequest; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.naming.directory.DirContext; +import javax.naming.ldap.LdapContext; + +import java.util.List; + +import static org.springframework.ldap.query.LdapQueryBuilder.query; + +@Service +public class LdapService { + + @Resource + private LdapTemplate ldapTemplate; + + @Resource + private PersonRepoImpl personRepo; + + public boolean authenticate(LoginRequest request) { +// String userDn, String credentials + DirContext ctx = null; + String dn = null; + String username = request.getUsername(); + String credentials = request.getPassword(); + + List user = personRepo.findByName(username); + + if (user.size() > 0) { + dn = personRepo.getDnForUser(username); + } else { + MSException.throwException("no such user"); + } + try { + ctx = ldapTemplate.getContextSource().getContext(dn, credentials); +// ldapTemplate.authenticate(dn, credentials); + // Take care here - if a base was specified on the ContextSource + // that needs to be removed from the user DN for the lookup to succeed. + // ctx.lookup(userDn); + return true; + } catch (Exception e) { + // Context creation failed - authentication did not succeed + System.out.println("Login failed: " + e); + MSException.throwException("login failed..."); + return false; + } finally { + // It is imperative that the created DirContext instance is always closed + LdapUtils.closeContext((LdapContext) ctx); + } + } +} diff --git a/backend/src/main/java/io/metersphere/ldap/PersonRepo.java b/backend/src/main/java/io/metersphere/ldap/PersonRepo.java new file mode 100644 index 0000000000..d8cf48ac44 --- /dev/null +++ b/backend/src/main/java/io/metersphere/ldap/PersonRepo.java @@ -0,0 +1,13 @@ +package io.metersphere.ldap; + + +import java.util.List; + +public interface PersonRepo { + + List getAllPersonNames(); + + List findByName(String name); + + String getDnForUser(String name); +} diff --git a/backend/src/main/java/io/metersphere/ldap/PersonRepoImpl.java b/backend/src/main/java/io/metersphere/ldap/PersonRepoImpl.java new file mode 100644 index 0000000000..9e3d24295a --- /dev/null +++ b/backend/src/main/java/io/metersphere/ldap/PersonRepoImpl.java @@ -0,0 +1,82 @@ +package io.metersphere.ldap; + + +import io.metersphere.ldap.domain.Person; +import org.springframework.ldap.NamingException; +import org.springframework.ldap.core.*; +import org.springframework.ldap.core.support.AbstractContextMapper; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import javax.naming.directory.Attributes; + + +import java.util.List; + +import static org.springframework.ldap.query.LdapQueryBuilder.query; + +@Service +public class PersonRepoImpl implements PersonRepo { + + @Resource + private LdapTemplate ldapTemplate; + + + @Override + public List getAllPersonNames() { + ldapTemplate.setIgnorePartialResultException(true); + return ldapTemplate.search( + query().where("objectclass").is("person"), + new AttributesMapper() { + @Override + public String mapFromAttributes(Attributes attrs) + throws NamingException, javax.naming.NamingException { + return attrs.toString(); + } + }); + } + + @Override + public List findByName(String name) { + ldapTemplate.setIgnorePartialResultException(true); + LdapQuery query = query() +// .where("objectclass").is("person") +// .and("cn").is(name); + .where("cn").is(name); + return ldapTemplate.search(query, getContextMapper()); + } + + @Override + public String getDnForUser(String uid) { + List result = ldapTemplate.search( + query().where("cn").is(uid), + new AbstractContextMapper() { + @Override + protected String doMapFromContext(DirContextOperations ctx) { + return ctx.getNameInNamespace(); + } + }); + + if(result.size() != 1) { + throw new RuntimeException("User not found or not unique"); + } + + return result.get(0); + } + + protected ContextMapper getContextMapper() { + return new PersonContextMapper(); + } + + + private static class PersonContextMapper extends AbstractContextMapper { + @Override + public Person doMapFromContext(DirContextOperations context) { + Person person = new Person(); + person.setCommonName(context.getStringAttribute("cn")); + person.setSuerName(context.getStringAttribute("sn")); + return person; + } + } + +} diff --git a/backend/src/main/java/io/metersphere/ldap/controller/LdapController.java b/backend/src/main/java/io/metersphere/ldap/controller/LdapController.java new file mode 100644 index 0000000000..d8174a6384 --- /dev/null +++ b/backend/src/main/java/io/metersphere/ldap/controller/LdapController.java @@ -0,0 +1,54 @@ +package io.metersphere.ldap.controller; + +import io.metersphere.base.domain.User; +import io.metersphere.controller.ResultHolder; +import io.metersphere.controller.request.LoginRequest; +import io.metersphere.ldap.LdapService; +import io.metersphere.service.UserService; +import org.apache.shiro.SecurityUtils; +import org.springframework.boot.web.servlet.server.Session; +import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; + +import static io.metersphere.commons.constants.SessionConstants.ATTR_USER; + +@RestController +@RequestMapping("/ldap") +public class LdapController { + + @Resource + private UserService userService; + @Resource + private LdapService ldapService; + + @PostMapping(value = "/signin") + public ResultHolder login(@RequestBody LoginRequest request) { + ldapService.authenticate(request); + + SecurityUtils.getSubject().getSession().setAttribute("authenticate", "ldap"); + + String username = request.getUsername(); + String password = request.getPassword(); + + User u = userService.selectUser(request.getUsername()); + if (u == null) { + User user = new User(); + user.setId(username); + user.setName(username); + // todo user email ? + user.setEmail(username + "@fit2cloud.com"); + user.setPassword(password); + userService.createUser(user); + } else { + request.setUsername(u.getId()); + request.setPassword(u.getPassword()); + } + + return userService.login(request); + } + + + + + +} diff --git a/backend/src/main/java/io/metersphere/ldap/domain/Person.java b/backend/src/main/java/io/metersphere/ldap/domain/Person.java new file mode 100644 index 0000000000..2dcb9eca49 --- /dev/null +++ b/backend/src/main/java/io/metersphere/ldap/domain/Person.java @@ -0,0 +1,22 @@ +package io.metersphere.ldap.domain; + +import lombok.Data; +import org.springframework.ldap.odm.annotations.Attribute; +import org.springframework.ldap.odm.annotations.DnAttribute; +import org.springframework.ldap.odm.annotations.Id; + +import javax.naming.Name; + +@Data +public class Person { + + @Id + private Name id; + @DnAttribute(value="uid",index = 3) + private String uid; + @Attribute(name = "cn") + private String commonName; + @Attribute(name = "sn") + private String suerName; + private String userPassword; +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/security/ShiroDBRealm.java b/backend/src/main/java/io/metersphere/security/ShiroDBRealm.java index ffd5e40035..0f9b899116 100644 --- a/backend/src/main/java/io/metersphere/security/ShiroDBRealm.java +++ b/backend/src/main/java/io/metersphere/security/ShiroDBRealm.java @@ -8,6 +8,7 @@ import io.metersphere.dto.UserDTO; import io.metersphere.i18n.Translator; import io.metersphere.service.UserService; import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -89,6 +90,14 @@ public class ShiroDBRealm extends AuthorizingRealm { SessionUtils.putUser(sessionUser); return new SimpleAuthenticationInfo(userId, password, getName()); } + + String login = (String) SecurityUtils.getSubject().getSession().getAttribute("authenticate"); + if (StringUtils.equals(login, "ldap")) { + SessionUser sessionUser = SessionUser.fromUser(user); + SessionUtils.putUser(sessionUser); + return new SimpleAuthenticationInfo(userId, password, getName()); + } + // 密码验证 if (!userService.checkUserPassword(userId, password)) { throw new IncorrectCredentialsException(Translator.get("password_is_incorrect")); diff --git a/backend/src/main/java/io/metersphere/service/UserService.java b/backend/src/main/java/io/metersphere/service/UserService.java index 934b4bbe2a..bc045d2267 100644 --- a/backend/src/main/java/io/metersphere/service/UserService.java +++ b/backend/src/main/java/io/metersphere/service/UserService.java @@ -10,6 +10,8 @@ import io.metersphere.commons.exception.MSException; import io.metersphere.commons.user.SessionUser; import io.metersphere.commons.utils.CodingUtil; import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.controller.ResultHolder; +import io.metersphere.controller.request.LoginRequest; import io.metersphere.controller.request.member.AddMemberRequest; import io.metersphere.controller.request.member.EditPassWordRequest; import io.metersphere.controller.request.member.QueryMemberRequest; @@ -20,7 +22,10 @@ import io.metersphere.dto.UserDTO; import io.metersphere.dto.UserRoleDTO; import io.metersphere.i18n.Translator; import org.apache.commons.lang3.StringUtils; -import org.apache.shiro.authc.DisabledAccountException; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.*; +import org.apache.shiro.authz.UnauthorizedException; +import org.apache.shiro.subject.Subject; import org.springframework.beans.BeanUtils; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -31,6 +36,8 @@ import javax.annotation.Resource; import java.util.*; import java.util.stream.Collectors; +import static io.metersphere.commons.constants.SessionConstants.ATTR_USER; + @Service @Transactional(rollbackFor = Exception.class) public class UserService { @@ -70,6 +77,10 @@ public class UserService { return getUserDTO(user.getId()); } + public User selectUser(String id) { + return userMapper.selectByPrimaryKey(id); + } + private void insertUserRole(List> roles, String userId) { for (int i = 0; i < roles.size(); i++) { Map map = roles.get(i); @@ -119,7 +130,7 @@ public class UserService { // password } - private void createUser(User userRequest) { + public void createUser(User userRequest) { User user = new User(); BeanUtils.copyProperties(userRequest, user); user.setCreateTime(System.currentTimeMillis()); @@ -460,4 +471,53 @@ public class UserService { public List getTestManagerAndTestUserList(QueryMemberRequest request) { return extUserRoleMapper.getTestManagerAndTestUserList(request); } + + public ResultHolder login(LoginRequest request) { + String msg; + String username = StringUtils.trim(request.getUsername()); + String password = StringUtils.trim(request.getPassword()); + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return ResultHolder.error("user or password can't be null"); + } + + UsernamePasswordToken token = new UsernamePasswordToken(username, password); + Subject subject = SecurityUtils.getSubject(); + + try { + subject.login(token); + if (subject.isAuthenticated()) { + UserDTO user = (UserDTO) subject.getSession().getAttribute(ATTR_USER); + // 自动选中组织,工作空间 + if (StringUtils.isEmpty(user.getLastOrganizationId())) { + List userRoles = user.getUserRoles(); + List test = userRoles.stream().filter(ur -> ur.getRoleId().startsWith("test")).collect(Collectors.toList()); + List org = userRoles.stream().filter(ur -> ur.getRoleId().startsWith("org")).collect(Collectors.toList()); + if (test.size() > 0) { + String wsId = test.get(0).getSourceId(); + switchUserRole("workspace", wsId); + } else if (org.size() > 0) { + String orgId = org.get(0).getSourceId(); + switchUserRole("organization", orgId); + } + } + // 返回 userDTO + return ResultHolder.success(subject.getSession().getAttribute("user")); + } else { + return ResultHolder.error(Translator.get("login_fail")); + } + } catch (ExcessiveAttemptsException e) { + msg = Translator.get("excessive_attempts"); + } catch (LockedAccountException e) { + msg = Translator.get("user_locked"); + } catch (DisabledAccountException e) { + msg = Translator.get("user_has_been_disabled"); + } catch (ExpiredCredentialsException e) { + msg = Translator.get("user_expires"); + } catch (AuthenticationException e) { + msg = e.getMessage(); + } catch (UnauthorizedException e) { + msg = Translator.get("not_authorized") + e.getMessage(); + } + return ResultHolder.error(msg); + } } diff --git a/frontend/src/business/components/settings/personal/PersonSetting.vue b/frontend/src/business/components/settings/personal/PersonSetting.vue index 07050c3259..05b2cbbba2 100644 --- a/frontend/src/business/components/settings/personal/PersonSetting.vue +++ b/frontend/src/business/components/settings/personal/PersonSetting.vue @@ -154,6 +154,17 @@ this.form = Object.assign({}, row); }, editPassword(row) { + this.$get("ldap/test", res => { + console.log(res) + }) + + this.$get("ldap/find/admin", res => { + console.log(res) + }) + + this.$get("ldap/testUser", res => { + console.log(res) + }) this.editPasswordVisible = true; }, updateUser(updateUserForm) { diff --git a/frontend/src/login/Login.vue b/frontend/src/login/Login.vue index 8d2fdad0cd..dc3cc5a087 100644 --- a/frontend/src/login/Login.vue +++ b/frontend/src/login/Login.vue @@ -15,6 +15,12 @@ {{$t('commons.welcome')}}
+ + + LDAP + 普通登录 + + @@ -60,7 +66,8 @@ return { form: { username: '', - password: '' + password: '', + authenticate: 'normal' }, rules: { username: [ @@ -105,24 +112,43 @@ submit(form) { this.$refs[form].validate((valid) => { if (valid) { - this.$post("signin", this.form, response => { - saveLocalStorage(response); - let language = response.data.language; - - if (!language) { - this.$get("language", response => { - language = response.data; - localStorage.setItem(DEFAULT_LANGUAGE, language) - window.location.href = "/" - }) - } else { - window.location.href = "/" - } - }); + switch (this.form.authenticate) { + case "normal": + this.normalLogin(); + break; + case "ldap": + this.ldapLogin(); + break; + default: + this.normalLogin(); + } } else { return false; } }); + }, + normalLogin() { + this.$post("signin", this.form, response => { + saveLocalStorage(response); + this.getLanguage(response.data.language); + }); + }, + ldapLogin() { + this.$post("ldap/signin", this.form, response => { + saveLocalStorage(response); + this.getLanguage(response.data.language); + }); + }, + getLanguage(language) { + if (!language) { + this.$get("language", response => { + language = response.data; + localStorage.setItem(DEFAULT_LANGUAGE, language) + window.location.href = "/" + }) + } else { + window.location.href = "/" + } } } } @@ -174,7 +200,7 @@ } .form { - margin-top: 60px; + margin-top: 30px; padding: 0 40px; }