背景
这几天接到公司的一个需求,需要研发一个多租户系统,其中涉及到操作系统角色管理模块,用户在租户平台创建自定义角色(系统初始化的时候会自动创建一些角色,hdfs 组件管理员,admin,yarn 等等),当用户在平台创建角色的时候会同步到 ldap,ldap 接入 Linux 之后被其他用户关联了,就会在 Linux 拥有这些权限,基于此,所以学习下 ldap 接入 SpringBoot 用法,简单写个 CRUD
安装 LDAP
还不知道怎么安装和了解 LDAP 的可用看看https://www.xiaohugg.top/articles/2023/07/11/1689057905882.html
开发
1.spring yam 文件配置
spring:
ldap:
urls: ldap://xxxxx:389
base: dc=xxx,dc=com
username: cn=example,dc=xxx,dc=xx
password: xxxxx
2.pom 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
3.controller 应用层代码
@RestController
@RequestMapping("/os/")
@RequiredArgsConstructor
public class OSUserGroupController {
private final OSUserGroupService oSUserGroupService;
@GetMapping("/pageLdap")
public Result<PageInfo<OSUserGroupDto>> queryPageLdap(@RequestParam(required = false) Integer gidNumber,
@RequestParam(required = false) String roleName,
@RequestParam int pageNo,
@RequestParam int pageSize) {
return Result.success(oSUserGroupService.queryPageLdap(gidNumber,roleName,pageNo,pageSize));
}
@PostMapping("/save")
public Result<Void> save(@RequestBody OSUserGroupDto osUserGroupDto) {
oSUserGroupService.save(osUserGroupDto);
return Result.success();
}
@PostMapping("/update")
public Result<Void> update(@RequestBody OSUserGroupDto osUserGroupDto) {
oSUserGroupService.update(osUserGroupDto);
return Result.success();
}
@PostMapping("/delete/{roleName}")
public Result<Void> deleteByRoleName(@PathVariable String roleName) {
oSUserGroupService.deleteByRoleName(roleName);
return Result.success();
}
@GetMapping("/queryAll")
public Result<List<OSUserGroupDto>> queryAll() {
return Result.success(oSUserGroupService.queryAll());
}
}
4.service 应用层代码
/**
* @Version 1.0
* @Author hu
* @Description OSUserGroupServiceImpl
* @Date 2023/10/24 9:31
**/
@Service
@RequiredArgsConstructor
public class OSUserGroupServiceImpl implements OSUserGroupService {
private final OSUserGroupDomainService osUserGroupDomainService;
@Override
public PageInfo<OSUserGroupDto> queryPageLdap(Integer id, String roleName, int pageNo, int pageSize) {
Page<OSUserGroupDto> page = osUserGroupDomainService.queryPageLdap(id,roleName,pageNo,pageSize);
if (Objects.isNull(page)) {
return new PageInfo<>();
}
PageInfo<OSUserGroupDto> pageInfo = new PageInfo<>(pageNo,pageSize);
pageInfo.setTotalList(page.toList());
pageInfo.setTotalPage(page.getTotalPages());
return pageInfo;
}
@Override
public void save(OSUserGroupDto osUserGroupDto) {
//判断是否是admin
osUserGroupDomainService.save(osUserGroupDto);
}
@Override
public void update(OSUserGroupDto osUserGroupDto) {
osUserGroupDomainService.update(osUserGroupDto);
}
@Override
public void deleteByRoleName(String roleName) {
if (roleName == null || roleName.isEmpty()) {
return;
}
osUserGroupDomainService.deleteByRoleName(roleName);
}
@Override
public List<OSUserGroupDto> queryAll() {
return osUserGroupDomainService.queryAll();
}
}
5.domain 领域层代码
import cn.hutool.core.collection.CollUtil;
import com.dtsw.tenant.api.domain.osusergroup.OSUserGroupDomainService;
import com.dtsw.tenant.api.domain.osusergroup.dto.OSUserGroupDto;
import com.dtsw.tenant.api.domain.common.PageInfo;
import com.dtsw.tenant.domain.osusergroup.entity.OSSudoers;
import com.dtsw.tenant.domain.osusergroup.entity.OSUserGroup;
import com.dtsw.tenant.domain.osusergroup.repository.OSUserGroupRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @Version 1.0
* @Author huqiang
* @Description UserGroupDomainServiceImpl
* @Date 2023/10/24 9:27
**/
@Service
@RequiredArgsConstructor
public class OSUserGroupDomainServiceImpl implements OSUserGroupDomainService {
private final OSUserGroupRepository osUserGroupRepository;
@Override
public PageInfo<OSUserGroupDto> queryPageLdap(Integer id, String roleName, int pageNo, int pageSize) {
List<OSUserGroup> osUserGroups = osUserGroupRepository.findPageLdap(id, roleName);
if (CollUtil.isEmpty(osUserGroups)) {
return null;
}
List<OSUserGroupDto> osUserGroupList = osUserGroups.stream().map(it -> {
OSUserGroupDto osUserGroupDto = new OSUserGroupDto();
return osUserGroupDto.create(it.getGidNumber(), it.getRoleName(), it.getDescription(),
it.getCreateTime(), it.getUpdateTime());
}).toList();
// 排序和分页
int offset = (pageNo - 1) * pageSize;
List<OSUserGroupDto> result = osUserGroupList.stream()
.sorted(Comparator.comparing(OSUserGroupDto::getGidNumber).reversed())
.skip(offset)
.limit(pageSize)
.toList();
//获取权限值sudoCommands
setSudoCommand(result);
PageInfo<OSUserGroupDto> pageInfo = new PageInfo<>(pageNo,pageSize);
pageInfo.setTotalList(result);
return pageInfo;
}
@Override
public synchronized void save(OSUserGroupDto osUserGroupDto) {
//名称是否重复
validateUserGroupNameExists(osUserGroupDto.getRoleName(), null);
var gidNumber = 0;
// 获取当前最大的gidNumber
List<OSUserGroup> ldap = osUserGroupRepository.findPageLdap(null, null);
if (CollUtil.isNotEmpty(ldap)) {
gidNumber = ldap.stream().mapToInt(OSUserGroup::getGidNumber).max().orElse(1300) + 1;
}
osUserGroupDto.setGidNumber(gidNumber);
var osUserGroup = OSUserGroup.valueOf(osUserGroupDto);
var osSudoers = OSSudoers.valueOf(osUserGroupDto.getRoleName(), osUserGroupDto.generateSudoCommandList());
osUserGroupRepository.save(osUserGroup, osSudoers);
}
@Override
public void update(OSUserGroupDto osUserGroupDto) {
//名称是否重复
validateUserGroupNameExists(osUserGroupDto.getRoleName(), osUserGroupDto.getGidNumber());
var osUserGroup = OSUserGroup.valueOfUpdate(osUserGroupDto);
var osSudoers = OSSudoers.valueOf(osUserGroupDto.getRoleName(),osUserGroupDto.generateSudoCommandList());
osUserGroupRepository.update(osUserGroup, osSudoers);
}
@Override
public void deleteByRoleName(String roleName) {
osUserGroupRepository.deleteByRoleName(roleName);
}
@Override
public List<OSUserGroupDto> queryAll() {
List<OSUserGroup> ldap = osUserGroupRepository.findPageLdap(null, null);
if (CollUtil.isEmpty(ldap)) {
return Collections.emptyList();
}
List<OSUserGroupDto> osUserGroupList = ldap.stream().map(it -> {
OSUserGroupDto osUserGroupDto = new OSUserGroupDto();
return osUserGroupDto.create(it.getGidNumber(), it.getRoleName(), it.getDescription(),
it.getCreateTime(), it.getUpdateTime());
}).toList();
//获取权限值sudoCommands
setSudoCommand(osUserGroupList);
return osUserGroupList;
}
private void setSudoCommand(List<OSUserGroupDto> osUserGroupList) {
for (OSUserGroupDto osUserGroupDto : osUserGroupList) {
List<String> sudoCommands = osUserGroupRepository.findListSudoCommandByUserGroup(osUserGroupDto.getRoleName());
osUserGroupDto.setAllowSudoCommands(sudoCommands.stream().filter(it -> !it.startsWith("!")).toList());
osUserGroupDto.setForbiddenSudoCommands(sudoCommands.stream().filter(it -> it.startsWith("!")).toList());
}
}
private void validateUserGroupNameExists(String roleName, Integer gidNumber) {
var osUserGroup = osUserGroupRepository.findUserGroupByRoleName(roleName);
if (Objects.isNull(osUserGroup)) {
return;
}
if (osUserGroup.getGidNumber().equals(gidNumber)) {
return;
}
throw new IllegalArgumentException("名称已存在:" + roleName);
}
}
6.基础设施层代码
package com.dtsw.tenant.infra.dao.osusergroup;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import com.dtsw.tenant.domain.osusergroup.common.OSUserGroupConstant;
import com.dtsw.tenant.domain.osusergroup.entity.OSSudoers;
import com.dtsw.tenant.domain.osusergroup.entity.OSUserGroup;
import com.dtsw.tenant.domain.osusergroup.repository.OSUserGroupRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.stereotype.Repository;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @Version 1.0
* @Author huqiang
* @Description OSUserGroupRepositoryImpl
* @Date 2023/10/24 10:06
**/
@Repository
@RequiredArgsConstructor
public class OSUserGroupRepositoryImpl implements OSUserGroupRepository {
@Value("${forbidden.user_group}")
private String forbiddenDeleteUserGroup;
public static final String CN_FORMAT = "cn=%s,%s";
private final LdapTemplate ldapTemplate;
@Override
public List<OSUserGroup> findPageLdap(Integer gidNumber, String roleName) {
LdapQueryBuilder queryBuilder = LdapQueryBuilder.query()
.base(OSUserGroupConstant.OS_USER_GROUP_BASE);
AndFilter filter = new AndFilter();
if (roleName != null) {
filter.and(new LikeFilter("cn", roleName));
}
if (gidNumber != null) {
filter.and(new EqualsFilter("gidNumber", gidNumber));
}
LdapQuery query = queryBuilder.filter(filter.encode());
return ldapTemplate.find(query,OSUserGroup.class);
}
@Override
public List<String> findListSudoCommandByUserGroup(String roleName) {
OSSudoers osSudoers = getOsSudoers(roleName);
return osSudoers == null ? Collections.emptyList() : osSudoers.getSudoCommand();
}
private OSSudoers getOsSudoers(String roleName) {
String base = String.format("cn=%s%s%s", roleName, ",", OSUserGroupConstant.OS_USER_GROUP_SUDO_BASE);
LdapQueryBuilder queryBuilder = LdapQueryBuilder.query()
.base(base);
queryBuilder.where("sudoUser").is("%" + roleName);
try {
return ldapTemplate.findOne(queryBuilder,OSSudoers.class);
} catch (NameNotFoundException | EmptyResultDataAccessException e) {
//ldap如果没有该条目会报错,兼容之前创建的用户组没有配置sudoers
return null;
}
}
@Override
public void save(OSUserGroup osUserGroup, OSSudoers osSudoers) {
try {
ldapTemplate.create(osUserGroup);
ldapTemplate.create(osSudoers);
} catch (Exception e) {
delete(osUserGroup, osSudoers);
throw e;
}
}
@Override
public OSUserGroup findUserGroupByRoleName(String roleName) {
LdapQueryBuilder queryBuilder = LdapQueryBuilder.fromQuery(LdapQueryBuilder.query()
.base(OSUserGroupConstant.OS_USER_GROUP_BASE)
.where("cn")
.is(roleName));
List<OSUserGroup> osUserGroups = ldapTemplate.find(queryBuilder, OSUserGroup.class);
return CollUtil.isEmpty(osUserGroups) ? null : osUserGroups.get(0);
}
@Override
public void update(OSUserGroup osUserGroup, OSSudoers osSudoers) {
List<OSUserGroup> osUserGroups = findPageLdap(osUserGroup.getGidNumber(), null);
if (CollUtil.isEmpty(osUserGroups)) {
throw new IllegalArgumentException("数据不存在");
}
OSSudoers sudoersLdap = getOsSudoers(osUserGroups.get(0).getRoleName());
if (Objects.isNull(sudoersLdap)) {
throw new IllegalArgumentException("数据不存在");
}
try {
save(osUserGroup, osSudoers);
} catch (Exception e) {
//插入出现异常,回滚本次新插入的数据
delete(osUserGroup, osSudoers);
} finally {
//插入成功后,删除之前的数据
delete(osUserGroups.get(0), sudoersLdap);
}
}
@Override
public void deleteByRoleName(String roleName) {
if (roleName == null || roleName.isEmpty()) {
return;
}
//判断是否是初始化角色,无法删除
Assert.isTrue(!Arrays.asList(forbiddenDeleteUserGroup.split(",")).contains(roleName),
()-> new IllegalArgumentException("默认角色无法删除"));
OSSudoers sudoers = getOsSudoers(roleName);
OSUserGroup osUserGroup = findUserGroupByRoleName(roleName);
Assert.isTrue(CollUtil.isEmpty(osUserGroup.getMemberUid()),
() -> new IllegalArgumentException("该角色被用户关联 " + osUserGroup.getMemberUid()));
//断言没有关联租户
delete(osUserGroup, sudoers);
}
private void delete(OSUserGroup osUserGroup, OSSudoers osSudoers) {
if (Objects.nonNull(osSudoers)) {
ldapTemplate.delete(osSudoers);
}
if (Objects.nonNull(osUserGroup)) {
ldapTemplate.delete(osUserGroup);
}
}
}
7.实体对象
package com.dtsw.tenant.domain.osusergroup.entity;
import com.dtsw.tenant.api.domain.osusergroup.dto.OSUserGroupDto;
import com.dtsw.tenant.domain.osusergroup.OSUserGroupConverter;
import lombok.Data;
import org.springframework.ldap.odm.annotations.*;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* @Version 1.0
* @Author huqiang
* @Description OSUserGroup
* @Date 2023/10/24 10:09
**/
@Data
@Entry(objectClasses = {"posixGroup"} )
public class OSUserGroup {
@Id
private Name dn;
@Attribute(name = "cn")
private String roleName;
@Attribute(name = "gidNumber")
private Integer gidNumber;
@Attribute(name = "description")
private String description;
@Attribute(name = "createTime")
@Transient
private Date createTime;
@Attribute(name = "updateTime")
@Transient
private Date updateTime;
@Transient
private List<String> sudoCommands;
@Attribute(name = "memberUid")
private List<String> memberUid;
public void setDn(String dn) {
this.dn = LdapNameBuilder.newInstance(dn).build();
}
public static OSUserGroup valueOf(OSUserGroupDto osUserGroupDto) {
validateRoleName(osUserGroupDto.getRoleName());
return OSUserGroupConverter.INSTANCE.fromDto(osUserGroupDto);
}
public static OSUserGroup valueOfUpdate(OSUserGroupDto osUserGroupDto) {
validateRoleName(osUserGroupDto.getRoleName());
return OSUserGroupConverter.INSTANCE.fromDtoUpdate(osUserGroupDto);
}
private static void validateRoleName(String roleName) {
String chinese = "^[^\\u4e00-\\u9fa5]*$";
if (roleName == null || roleName.isEmpty() || !roleName.matches(chinese)) {
throw new IllegalArgumentException("roleName不允许为空或者不能有中文,ldap sudoUser不支持 " + roleName);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OSUserGroup that = (OSUserGroup) o;
return Objects.equals(dn, that.dn);
}
@Override
public int hashCode() {
return Objects.hash(dn);
}
}
package com.dtsw.tenant.domain.osusergroup.entity;
import cn.hutool.core.collection.CollUtil;
import com.dtsw.tenant.domain.osusergroup.common.OSUserGroupConstant;
import lombok.Data;
import org.springframework.ldap.odm.annotations.*;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
import java.util.List;
import java.util.Objects;
/**
* @Version 1.0
* @Author huqiang
* @Description OSSudoers
* @Date 2023/10/24 12:36
**/
@Data
@Entry(objectClasses = {"sudoRole","top"} )
public class OSSudoers {
@Id
private Name dn;
@Attribute(name = "sudoCommand")
private List<String> sudoCommand;
@Attribute(name = "sudoHost")
private String sudoHost = "ALL";
@Attribute(name = "sudoOption")
private String sudoOption = "!authenticate";
@Attribute(name = "sudoRunAsUser")
private String sudoRunAsUser = "ALL";
@Attribute(name = "sudoUser")
private String sudoUser;
@DnAttribute(value = "cn",index = 0)
@Attribute(name = "cn")
private String roleName;
public void setDn(String dn) {
this.dn = LdapNameBuilder.newInstance(dn).build();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OSSudoers osSudoers = (OSSudoers) o;
return Objects.equals(dn, osSudoers.dn);
}
@Override
public int hashCode() {
return Objects.hash(dn);
}
public static OSSudoers valueOf(String roleName, List<String> sudoCommands) {
OSSudoers osSudoers = new OSSudoers();
osSudoers.setRoleName(roleName);
osSudoers.setSudoCommand(CollUtil.isEmpty(sudoCommands)? null : sudoCommands);
osSudoers.setSudoUser("%" + roleName);
osSudoers.setDn(String.format("cn=%s,%s",roleName, OSUserGroupConstant.OS_USER_GROUP_SUDO_BASE));
return osSudoers;
}
}
实体转换
涉及到领域驱动,有许多 dto,vo,bo 实体相互转换,目前有 apache 的 beanUtils 和 Spring 的 beanUtils,这两个都是基于反射构建,所以引入了 mapstruct
package com.dtsw.tenant.domain.osusergroup;
import com.dtsw.tenant.api.domain.osusergroup.dto.OSUserGroupDto;
import com.dtsw.tenant.domain.osusergroup.common.OSUserGroupConstant;
import com.dtsw.tenant.domain.osusergroup.entity.OSUserGroup;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/**
* @Version 1.0
* @Author huqiang
* @Description OSUserGroupConverter
* @Date 2023/10/27 16:22
**/
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface OSUserGroupConverter {
OSUserGroupConverter INSTANCE = Mappers.getMapper(OSUserGroupConverter.class);
@Mapping(target = "createTime",expression = "java(new java.util.Date())")
@Mapping(target = "updateTime",expression = "java(new java.util.Date())")
@Mapping(target = "dn", expression = "java(setDn(dto.getRoleName()))")
OSUserGroup fromDto(OSUserGroupDto dto);
@Mapping(target = "updateTime",expression = "java(new java.util.Date())")
@Mapping(target = "dn",expression = "java(setDn(dto.getRoleName()))")
OSUserGroup fromDtoUpdate(OSUserGroupDto dto);
@Named("setDn")
default String setDn(String roleName) {
return String.format("cn=%s,%s", roleName, OSUserGroupConstant.OS_USER_GROUP_BASE);
}
}
ldap 客户端查看效果
结尾
至此一个简单的 ldap CRUD 小 case 基本上开发完毕,ldap 还有很多高级特性和复杂的业务,需要后续发掘