Как работает пользовательский логин Spring Security

#java #authentication #login #spring-security #logout

#java #аутентификация #войти #spring-безопасность #Выход

Вопрос:

Я пытаюсь пройти через Spring Security. Я должен реализовать пользовательскую форму входа, поэтому мне нужно очень хорошо понимать, что означают мои конфигурации.

spring-security.xml

 <beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-4.0.xsd">

    <http auto-config="true">
        <intercept-url pattern="/user**" access="isAuthenticated()" />
        <form-login authentication-failure-url="/login" login-page="/login"
            login-processing-url="/login" default-target-url="/user" />
        <logout invalidate-session="true" logout-success-url="/index"
            logout-url="/logout" />
    </http>

    <authentication-manager id="custom-auth">
        <authentication-provider>
            <user-service>
                <user name="my_username" password="my_password"
                    authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>
 

LoginController

 @Controller
public class LoginController {
    [....]

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ModelAndView doLogin() {
        System.out.println("***LOGIN_POST***");
        return new ModelAndView("users/home");
    }

    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    public ModelAndView doLogout() {
        System.out.println("***LOGOUT_POST***");
        return new ModelAndView("index");
    }
}
 

Я знаю, что могу сопоставить URL-адрес /login с RequestMethod .ПОЛУЧИТЬ, но когда я пытаюсь перехватить сообщение после отправки формы, оно не работает.

  1. Я считаю, но должен подтвердить, что это связано с тем, что Security делает что-то за кулисами: получает значения имени пользователя и пароля из опубликованной формы и сравнивает их с значениями в поставщике аутентификации: если они совпадают, отображается default-target-url, иначе пользователь должен повторить вход в систему. Правильно ли это?
  2. Тогда моя проблема: мне нужны значения имени пользователя и пароля, введенные в форме входа в систему безопасности, потому что я должен отправить HTTP-запрос на внешний сервер, чтобы проверить, совпадают ли они. Прежде чем вводить безопасность, я разработал этот механизм, используя /login GET и /login POST с аннотацией @ModelAttribute . Что я могу сделать сейчас?
  3. Изменение поставщика аутентификации с использованием класса, который реализует UserDetailsService, что происходит? Я полагаю, что в этом случае имя пользователя и пароль, введенные в форме входа, сравниваются с теми, которые были получены из базы данных, поскольку они назначены объекту User. Правильно ли это?

UserDetailsServiceImpl

     @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
        @Autowired
        private CustomerDao customerDao;

        @Override
        public UserDetails loadUserByUsername(String username)
                throws UsernameNotFoundException {
            Customer customer = customerDao.findCustomerByUsername(username);
            return new User(customer.getUsername(), customer.getPassword(), true, true, true, true,
                Arrays.asList(new SimpleGrantedAuthority(customer.getRole())));
        }
    }
 


Сначала данные пользователя не находятся в моей базе данных, это потому, что я не уверен в решении UserDetailsService (в котором пользовательские данные загружаются просто по имени пользователя).
Для получения моего объекта Customer мне нужны как имя пользователя, так и пароль (для отправки на определенный внешний URL-адрес), затем, если ответ JSON положительный (имя пользователя и пароль верны), я должен отправить 2 других HTTP-запроса, чтобы получить данные клиента в виде имени, фамилии, национальности и т.д. На данный момент мой пользователь может считаться вошедшим в систему.

Есть предложения? Заранее спасибо.

Ответ №1:

  1. Я считаю, но должен подтвердить, что это связано с тем, что Security делает что-то за кулисами: получает значения имени пользователя и пароля из опубликованной формы и сравнивает их с значениями в поставщике аутентификации: если они совпадают, отображается default-target-url, иначе пользователь должен повторить вход в систему. Правильно ли это?

Это верно. Когда вы объявляете <login-form> элемент в конфигурации безопасности, вы настраиваете UsernamePasswordAuthenticationFilter .

Там вы настраиваете некоторые URL-адреса:

  • login-page=»/login»: URL-адрес, который указывает на a @RequestMapping , который возвращает форму входа
  • login-processing-url=»/login»: URL, который запускает UsernamePasswordAuthenticationFilter . Это spring-security эквивалентно созданию метода контроллера постобработки.
  • default-target-url=»/user»: страница по умолчанию, на которую пользователь будет перенаправлен после предоставления действительных учетных данных пользователя.
  • ошибка аутентификации-url=»/ login»: URL, на который пользователь будет перенаправлен в случае попытки входа с неверными учетными данными.

В то время как URL-адреса для обработки входа, URL-адреса по умолчанию и URL-адреса для проверки подлинности должны быть допустимыми приложениями для запросов, URL-адрес для обработки входа не достигнет уровня контроллера Spring MVC, поскольку он выполняется перед запуском сервлета диспетчера Spring MVC.

Итак,

 @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ModelAndView doLogin() {
        System.out.println("***LOGIN_POST***");
        return new ModelAndView("users/home");
    }
 

никогда не будет достигнут.

Когда POST отправляется в /login uri, UsernamePasswordAuthenticationFilter выполнит doFilter() свой метод, чтобы перехватить предоставленные пользователем учетные данные, создать UsernamePasswordAuthenticationToken и делегировать его AuthenticationManager, где эта аутентификация будет выполнена в соответствующем AuthenticationProvider.

  1. Тогда моя проблема: мне нужны значения имени пользователя и пароля, введенные в форме входа в систему безопасности, потому что я должен отправить HTTP-запрос на внешний сервер, чтобы проверить, совпадают ли они. Прежде чем вводить безопасность, я разработал этот механизм, используя /login GET и /login POST с аннотацией @ModelAttribute . Что я могу сделать сейчас? Я полагаю, что когда вы выполняли аутентификацию на внешнем сервере, вы делали это, делегируя классу из POST / login RequestMapping .

Итак, просто создайте пользовательский AuthenticationProvider, который делегирует материал проверки пользователя вашей старой логике:

 public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {
    
    private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;
    
    // This represents your existing username/password validation class
    // Bind it with an @Autowired or set it in your security config
    private ExternalAuthenticationValidator externalAuthenticationValidator;

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        boolean validated = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
        if(!validated){
            throw new BadCredentialsException("username and/or password not valid");
        }
        Collection<? extends GrantedAuthority> authorities = null; 
        // you must fill this authorities collection
        return new UsernamePasswordAuthenticationToken(
                    authentication.getName(),
                    authentication.getCredentials(),
                    authorities
                );      
    }

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return this.supportingClass.isAssignableFrom(authentication);
    }

    public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
        return externalAuthenticationValidator;
    }

    public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
        this.externalAuthenticationValidator = externalAuthenticationValidator;
    }   

}
 

И xml конфигурации безопасности:

     <beans:bean id="thirdPartyAuthenticationProvider" class="com.xxx.yyy.ThirdPartyAuthenticationProvider">
        <!-- here set your external authentication validator in case you can't autowire it -->
        <beans:property name="externalAuthenticationValidator" ref="yourExternalAuthenticationValidator" />
    </beans:bean>
    
    <security:authentication-manager id="custom-auth">
        <security:authentication-provider ref="thirdPartyAuthenticationProvider" />
    </security:authentication-manager>
    
    <security:http auto-config="true" authentication-manager-ref="custom-auth">
        <security:intercept-url pattern="/user**" access="isAuthenticated()" />
        <security:form-login authentication-failure-url="/login" login-page="/login"
            login-processing-url="/login" default-target-url="/user" />
        <security:logout invalidate-session="true" logout-success-url="/index"
            logout-url="/logout" />
        <!-- in spring security 4.x CSRF filter is enabled by default. Disable it if 
             you don't plan to use it, or at least in the first attempts -->
        <security:csrf disabled="true"/>
    </security:http>
 
  1. Изменение поставщика аутентификации с использованием класса, который реализует UserDetailsService, что происходит? Я полагаю, что в этом случае имя пользователя и пароль, введенные в форме входа, сравниваются с теми, которые были получены из базы данных, поскольку они назначены объекту User. Правильно ли это?

Поскольку вы сказали, что вы должны отправить как имя пользователя, так и пароль, я не думаю, что схема UserServiceDetails соответствует вашим требованиям. Я думаю, вам следует сделать это так, как я предложил в пункте 2.

Редактировать:

И последнее: теперь я отправляю HTTP-запрос в методе аутентификации, если учетные данные верны, я получаю токен в ответе, который мне нужен для получения доступа к другим внешним серверным службам. Как я могу передать его в свой Spring controller?

Чтобы получить и обработать полученный токен, я бы сделал это следующим образом:

Интерфейс ExternalAuthenticationValidator:

 public interface ExternalAuthenticationValidator {

    public abstract ThirdPartyValidationResponse validate(String name, String password);

}
 

Интерфейс модели третьей части validationresponse:

 public interface ThirdPartyValidationResponse{
    
    public boolean isValid();
    
    public Serializable getToken();

}
 

Затем измените способ, которым Поставщик обрабатывает его и управляет им:

 public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {
    
    private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;
    
    private ExternalAuthenticationValidator externalAuthenticationValidator;

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ThirdPartyValidationResponse response = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
        if(!response.isValid()){
            throw new BadCredentialsException("username and/or password not valid");
        }
        Collection<? extends GrantedAuthority> authorities = null; 
        // you must fill this authorities collection
        UsernamePasswordAuthenticationToken authenticated =  
                new UsernamePasswordAuthenticationToken(
                    authentication.getName(),
                    authentication.getCredentials(),
                    authorities
                );
        authenticated.setDetails(response);
        return authenticated;
    }

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return this.supportingClass.isAssignableFrom(authentication);
    }

    public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
        return externalAuthenticationValidator;
    }

    public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
        this.externalAuthenticationValidator = externalAuthenticationValidator;
    }   

}
 

Теперь вы должны использовать этот фрагмент кода для извлечения токена из пользовательских данных:

         SecurityContext context = SecurityContextHolder.getContext();
        Authentication auth = context.getAuthentication();
        if(auth == null){
            throw new IllegalAccessException("Authentication is null in SecurityContext");
        }
        if(auth instanceof UsernamePasswordAuthenticationToken){
            Object details = auth.getDetails();
            if(details != null amp;amp; details instanceof ThirdPartyValidationResponse){
                return ((ThirdPartyValidationResponse)details).getToken();
            }
        }
        return null;
 

Вместо того, чтобы включать его везде, где вам это нужно, может быть лучше создать класс, который извлекает его из деталей аутентификации:

 public class SecurityContextThirdPartyTokenRetriever {
    
    public Serializable getThirdPartyToken() throws IllegalAccessException{
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication auth = context.getAuthentication();
        if(auth == null){
            throw new IllegalAccessException("Authentication is null in SecurityContext");
        }
        if(auth instanceof UsernamePasswordAuthenticationToken){
            Object details = auth.getDetails();
            if(details != null amp;amp; details instanceof ThirdPartyValidationResponse){
                return ((ThirdPartyValidationResponse)details).getToken();
            }
        }
        return null;
    }

}
 

В случае, если вы выбрали этот последний способ, просто объявите его в конфигурации xml безопасности (или аннотируйте @Service аннотацией etc).:

 <beans:bean id="tokenRetriever" class="com.xxx.yyy.SecurityContextThirdPartyTokenRetriever" />
 

Существуют и другие подходы, такие как расширение UsernamePasswordAuthenticationToken для включения в него токена в качестве поля, но я думаю, что это самый простой.

Комментарии:

1. Спасибо за разъяснения! И последнее: теперь я отправляю HTTP-запрос в методе аутентификации, если учетные данные верны, я получаю токен в ответе, который мне нужен для получения доступа к другим внешним серверным службам. Как я могу передать его в свой Spring controller?

2. Сработало! Большое спасибо