Skip to content

Авторизация пользователй

Луна поддерживает внешнее управление пользователями. Вы можете добавить свой тип каталога пользователй, реализовав интерфейс DirectoryType.

Пример реализации для LDAP:

java
package ru.slie.luna.directory.impl;

import org.apache.commons.lang3.StringUtils;
import org.springframework.ldap.UncategorizedLdapException;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.filter.HardcodedFilter;
import org.springframework.security.core.Authentication;
import ru.slie.luna.directory.Directory;
import ru.slie.luna.directory.DirectoryType;
import ru.slie.luna.directory.annotations.DirectoryTypeComponent;
import ru.slie.luna.directory.impl.ldap.LdapAttributesMapper;
import ru.slie.luna.directory.impl.ldap.LdapSecureMode;
import ru.slie.luna.directory.impl.ldap.LdapTlsDirContextAuthenticationStrategy;
import ru.slie.luna.directory.impl.ldap.UnsecuredSSLSocketFactory;
import ru.slie.luna.directory.user.DirectoryUser;
import ru.slie.luna.exception.ValidateException;
import ru.slie.luna.locale.I18nResolver;
import ru.slie.luna.security.SecretEncryptor;
import ru.slie.luna.security.exceptions.LunaAuthenticationException;
import ru.slie.luna.user.User;

import javax.naming.directory.SearchControls;
import java.util.*;

import static ru.slie.luna.directory.impl.ldap.LdapDirectoryAttributes.*;


@Component
public class LdapDirectoryType implements DirectoryType {
    private final I18nResolver i18n;
    private final SecretEncryptor secretEncryptor;

    public LdapDirectoryType(I18nResolver i18n,
                             SecretEncryptor secretEncryptor) {
        this.i18n = i18n;
        this.secretEncryptor = secretEncryptor;
    }

    @Override
    public String getName() {
        return i18n.getText("app.directory.ldap.label");
    }

    @Override
    public String getDescription() {
        return i18n.getText("app.directory.ldap.description");
    }

    @Override
    public Collection<DirectoryAction> getDirectoryActions() {
        // Разрешаем создавать пользователя при первой авторизации
        return List.of(DirectoryAction.CREATE_ON_AUTHENTICATION);
    }

    @Override
    public DirectoryUser getUserByLogin(Directory directory, String login) {
        LdapContextSource contextSource = getContextSource(directory);
        LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        searchControls.setCountLimit(1000);
        AndFilter filter = new AndFilter();
        filter.and(new HardcodedFilter(directory.getAttribute(USER_FILTER)));
        filter.and(new EqualsFilter(directory.getAttribute(USER_NAME), login));
        try {
            List<DirectoryUser> users = ldapTemplate.search("", filter.encode(), searchControls, new LdapAttributesMapper(directory.getAttributes()));
            if (!users.isEmpty()) {
                return users.getFirst();
            }
        } catch (UncategorizedLdapException ignored) {
            return null;
        }
        return null;
    }

    @Override
    public void authenticate(User user, Directory directory, Authentication authentication) throws LunaAuthenticationException {
        String externalId = user.getExternalId();
        if (authentication.getCredentials() == null) {
            throw new LunaAuthenticationException();
        }

        String password = authentication.getCredentials().toString();

        LdapContextSource contextSource = getContextSource(directory);
        LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
        String externalIDAttr = directory.getAttribute(EXTERNAL_ID);
        AndFilter filter = new AndFilter();
        filter.and(new HardcodedFilter(directory.getAttribute(USER_FILTER)));
        filter.and(LdapAttributesMapper.makeFilter(externalIDAttr, externalId));

        if (!ldapTemplate.authenticate("", filter.encode(), password)) {
            throw new LunaAuthenticationException();
        }
    }

    /**
    * Аутентификация для новых пользователей
    */
    @Override
    public DirectoryUser authenticate(Directory directory, Authentication authentication) throws LunaAuthenticationException {
        if (authentication.getPrincipal() == null || authentication.getCredentials() == null) {
            throw new LunaAuthenticationException();
        }
        String login = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();

        DirectoryUser user = getUserByLogin(directory, login);
        if (user == null) {
            throw new LunaAuthenticationException();
        }
        LdapContextSource contextSource = getContextSource(directory);
        LdapTemplate ldapTemplate = new LdapTemplate(contextSource);
        Filter filter = new EqualsFilter(directory.getAttribute(USER_NAME), login);
        if (ldapTemplate.authenticate("", filter.encode(), password)) {
            if (directory.getAttribute(DEFAULT_GROUPS) != null) {
                List<String> groups = Arrays.stream(directory.getAttribute(DEFAULT_GROUPS).split(",")).map(String::trim).filter(g -> !g.isEmpty()).toList();
                user.setGroups(groups);
            }
            return user;
        }

        throw new LunaAuthenticationException();
    }

    /**
    * Переопределите getDirectoryActions что бы разрешить создавать пользователй во внешнем каталоге
    */
    @Override
    public DirectoryUser createUser(Directory directory, DirectoryUser directoryUser) {
        return null;
    }

    /**
    * Переопределите getDirectoryActions что бы разрешить изменять пользователй во внешнем каталоге
    */
    @Override
    public DirectoryUser updateUser(Directory directory, DirectoryUser directoryUser) {
        return null;
    }

    /**
    * Переопределите getDirectoryActions что бы разрешить удалять пользователй во внешнем каталоге
    */
    @Override
    public DirectoryUser removeUser(Directory directory, String login) {
        return null;
    }

    /**
    * Если ваш каталог содержит web-компонент для настроек, в этом методе вы можете валидировать параметры перед сохранением
    */
    @Override
    public void validateAttributes(Map<String, String> attributes) throws ValidateException {
        ValidateException exception = new ValidateException();

        if (!attributes.containsKey(HOST) || attributes.get(HOST).isEmpty()) {
            exception.addError(HOST, i18n.getText("app.field.host.required"));
        }

        if (!attributes.containsKey(PORT) || attributes.get(PORT).isEmpty() || !StringUtils.isNumeric(attributes.get(PORT))) {
            exception.addError(PORT, i18n.getText("app.field.port.required"));
        }
        ...
        exception.raise();
    }

    /**
    * Что бы не хранить пароль в отрытом виде в БД, вы можете зашифровать его м помощью secretEncryptor
    */
    @Override
    public void prepareSave(Map<String, String> attributes) {
        if (attributes.containsKey(PASSWORD) && attributes.get(PASSWORD) != null && !attributes.get(PASSWORD).isEmpty()) {
            attributes.put(PASSWORD, secretEncryptor.encrypt(attributes.get(PASSWORD)));
        }
    }

    /**
    * Не забываем декодировать зашифрованный пароль
    */
    @Override
    public void prepareLoad(Map<String, String> attributes) {
        if (attributes.containsKey(PASSWORD) && attributes.get(PASSWORD) != null && !attributes.get(PASSWORD).isEmpty()) {
            attributes.put(PASSWORD, secretEncryptor.decrypt(attributes.get(PASSWORD)));
        }
    }
}

Фронтенд компонент настроек

Что бы наш компонент поддерживал настройки, создадим web-компонент

vue
<script setup lang="ts">
import { onMounted, PropType, ref, watch } from 'vue'
import { Errors, TogglePanel, MultiSelect, Group  } from "luna";

interface LdapDirectoryAttributes {
  host: string,
  port: number,
  useSSL: string,
  userDn: string,
  password: string,
  baseDn: string,
  userFilter: string,
  userName: string,
  firstName: string,
  lastName: string,
  displayName: string,
  userEmail: string,
  userPassword: string,
  externalId: string,
  defaultGroups: string,
}

defineProps({
  errors: Object as PropType<Errors>
});

const value = defineModel<LdapDirectoryAttributes>()
const defaultGroups = ref<Array<string>>([]);

watch(defaultGroups, (val) => {
    value.value.defaultGroups = val.join(",")
  },
  {deep: true}
)

const groupOptions = async (term: string, excludes: Array<string>) => {
  const {data} = await groupService.findGroups({term}, excludes);
  return data.results.map((g: Group) => {return {id: g.name, name: g.name}});
}

onMounted(() => {
  if (value.value?.defaultGroups) {
    defaultGroups.value = value.value.defaultGroups.split(",").map((s: string) => s.trim());
  }
})
</script>

<template>
  <div class="internal-directory">
      <TogglePanel class="panel pad" :title="$i18n.t('Server settings')">
        <div class="field-group">
          <label for="ldap-directory-host" class="required">{{ $i18n.t("Host name") }}</label>
          <input id="ldap-directory-host" type="text" v-model="value.host" class="input">
          <div class="error" v-if="errors && errors['host']">{{ errors.host }}</div>
        </div>

        <div class="field-group">
          <label for="ldap-directory-port" class="required">{{ $i18n.t("Port") }}</label>
          <input id="ldap-directory-port" type="number" v-model="value.port" class="input">
          <div class="error" v-if="errors && errors['port']">{{ errors.port }}</div>
        </div>

        <div class="field-group">
          <strong>{{ $i18n.t("Use SSL") }}</strong>
          <fieldset class="checkbox-group">
            <div class="checkbox-item">
              <input id="ldap-directory-use-ssl" type="checkbox" v-model="value.useSSL" value="true">
              <label for="ldap-directory-use-ssl">{{ $i18n.t("Yes") }}</label>
            </div>
          </fieldset>
        </div>

        <div class="field-group">
          <label for="ldap-directory-user-dn">{{ $i18n.t("User name") }}</label>
          <input id="ldap-directory-user-dn" type="text" v-model="value.userDn" class="input">
          <div class="error" v-if="errors && errors['userDn']">{{ errors.userDn }}</div>
        </div>

        <div class="field-group">
          <label for="ldap-directory-password">{{ $i18n.t("Password") }}</label>
          <input id="ldap-directory-password" type="password" v-model="value.password" class="input">
          <div class="error" v-if="errors && errors['password']">{{ errors.password }}</div>
        </div>
      </TogglePanel>

      <TogglePanel class="panel pad" :title="$i18n.t('Security settings')">
        <div class="field-group">
          <label for="ldap-directory-groups">{{ $i18n.t("Default groups") }}</label>
          <MultiSelect id="ldap-directory-groups" v-model="defaultGroups" :options="groupOptions"></MultiSelect>
          <div class="error" v-if="errors && errors['defaultGroups']">{{ errors.defaultGroups }}</div>
        </div>
      </TogglePanel>
      ...
  </div>
</template>

Дескриптор плагина

yaml
directoryTypes:
  - key: LdapDirectoryType
    name: Ldap directory type
    className: ru.slie.luna.directory.impl.LdapDirectoryType
    webComponent:
      path: frontend/LdapDirectoryAttributes.js