文章 62
浏览 15135
随记springboot个ldap 实现CRUD

随记springboot个ldap 实现CRUD

背景

这几天接到公司的一个需求,需要研发一个多租户系统,其中涉及到操作系统角色管理模块,用户在租户平台创建自定义角色(系统初始化的时候会自动创建一些角色,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 还有很多高级特性和复杂的业务,需要后续发掘


标题:随记springboot个ldap 实现CRUD
作者:xiaohugg
地址:https://xiaohugg.top/articles/2023/10/25/1698212148105.html

人民有信仰 民族有希望 国家有力量