Авторизация пользователй
Луна поддерживает внешнее управление пользователями. Вы можете добавить свой тип каталога пользователй, реализовав интерфейс 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