Как вручную установить аутентифицированного пользователя в Spring Security / SpringMVC

107

После того, как новый пользователь отправит форму «Новая учетная запись», я хочу вручную выполнить вход для этого пользователя, чтобы ему не приходилось входить на следующей странице.

Обычная страница входа в систему, проходящая через перехватчик безопасности Spring, работает нормально.

В контроллере формы новой учетной записи я создаю UsernamePasswordAuthenticationToken и вручную устанавливаю его в SecurityContext:

SecurityContextHolder.getContext().setAuthentication(authentication);

На этой же странице я позже проверяю, что пользователь вошел в систему с:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Это возвращает полномочия, которые я установил ранее при аутентификации. Все хорошо.

Но когда этот же код вызывается на следующей странице, которую я загружаю, токен аутентификации будет просто UserAnonymous.

Я не понимаю, почему он не сохранил аутентификацию, которую я установил в предыдущем запросе. Есть предположения?

  • Может ли это быть связано с неправильной настройкой идентификатора сеанса?
  • Есть ли что-то, что может как-то перезаписать мою аутентификацию?
  • Возможно, мне просто нужен еще один шаг, чтобы сохранить аутентификацию?
  • Или мне нужно что-то сделать, чтобы как-то объявить аутентификацию для всего сеанса, а не для одного запроса?

Просто ищу некоторые мысли, которые могут помочь мне увидеть, что здесь происходит.

Дэвид Паркс
источник
1
Вы можете следить за моим ответом на stackoverflow.com/questions/4824395/…
AlexK
2
Читатели, берегитесь из ответов на этот вопрос , если они скажут вам , чтобы сделать: SecurityContextHolder.getContext().setAuthentication(authentication). Это работает и является обычным явлением, но есть серьезные функциональные недостатки, с которыми вы столкнетесь, если просто сделаете это. Для получения дополнительной информации см. Мой вопрос и ответ: stackoverflow.com/questions/47233187/…
goat

Ответы:

62

Некоторое время назад у меня была такая же проблема, как и у вас. Я не могу вспомнить детали, но следующий код помог мне. Этот код используется в потоке Spring Webflow, отсюда и классы RequestContext и ExternalContext. Но наиболее важная для вас часть - это метод doAutoLogin.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}
Кевин Стембридж
источник
2
Спасибо, код очень помогает мне понять, что я ищу неисправности в нужной области. Похоже, у меня есть дымящийся пистолет, он создает новый идентификатор сеанса после ручной аутентификации, но старый идентификатор сеанса все еще идентифицируется из файла cookie. Теперь нужно понять, почему, но, по крайней мере, я явно на правильном пути. Спасибо!
Дэвид Паркс
4
Любой, кто следует этому руководству, также должен увидеть эту связанную проблему: stackoverflow.com/questions/4824395/…
Дэвид Паркс
14
Не могли бы вы объяснить, как вы получаете AuthenticationProvider?
vivex
1
@ s1moner3d, вы сможете ввести его через IoC -> \ @Autowired
Хартмут
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
слизничий
66

Я не мог найти других полных решений, поэтому решил опубликовать свое. Это может быть чем-то вроде взлома, но он решил проблему с вышеуказанной проблемой:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
Стюарт Макинтайр
источник
7
+1 - Это мне помогло! Мне не хватало обновления SPRING_SECURITY_CONTEXT. ... Но насколько это "грязно"?
l3dx 01
12
откуда ты authenticationManager?
Isaac
2
authenticationManager автоматически подключается к вашему классу, как этот @Autowired AuthenticationServiceImpl authenticationManager. И в вашей конфигурации xml также должна быть инъекция bean-компонентов, чтобы Spring знал, что вводить.
1
где реализация AuthenticationServiceImpl? Что содержит этот класс?
Pra_A
3
Зачем нужно создавать новую сессию? Разве SecurityContext не справляется с этим?
Влад Мануэль Муредан
17

В конечном итоге выяснили корень проблемы.

Когда я создаю контекст безопасности вручную, объект сеанса не создается. Только когда запрос завершает обработку, механизм Spring Security понимает, что объект сеанса имеет значение NULL (когда он пытается сохранить контекст безопасности для сеанса после обработки запроса).

В конце запроса Spring Security создает новый объект сеанса и идентификатор сеанса. Однако этот новый идентификатор сеанса никогда не попадает в браузер, потому что он появляется в конце запроса, после того, как браузер получил ответ. Это приводит к потере нового идентификатора сеанса (и, следовательно, контекста безопасности, содержащего моего зарегистрированного вручную пользователя), когда следующий запрос содержит идентификатор предыдущего сеанса.

Дэвид Паркс
источник
4
Честно говоря, это больше похоже на недостаток дизайна весенней безопасности. Существует множество фреймворков, написанных на других языках, для которых не было бы проблем с этим, но Spring Security просто ломается.
chubbsondubs
3
а решение есть?
s1moner3d
2
и какое решение?
Тьяго
6

Включите ведение журнала отладки, чтобы лучше понять, что происходит.

Вы можете узнать, устанавливаются ли файлы cookie сеанса, используя отладчик на стороне браузера, чтобы просмотреть заголовки, возвращаемые в ответах HTTP. (Есть и другие способы.)

Одна из возможностей состоит в том, что SpringSecurity устанавливает файлы cookie безопасного сеанса, и ваша следующая запрашиваемая страница имеет URL-адрес http вместо URL-адреса https. (Браузер не отправляет безопасный файл cookie для URL-адреса "http".)

Стивен С
источник
Спасибо, это были очень полезные и актуальные предложения!
Дэвид Паркс
5

Новая функция фильтрации в Servlet 2.4 в основном снимает ограничение, согласно которому фильтры могут работать только в потоке запросов до и после фактической обработки запроса сервером приложений. Вместо этого фильтры Servlet 2.4 теперь могут взаимодействовать с диспетчером запросов в каждой точке отправки. Это означает, что когда веб-ресурс пересылает запрос другому ресурсу (например, сервлет, пересылающий запрос на страницу JSP в том же приложении), фильтр может работать до того, как запрос будет обработан целевым ресурсом. Это также означает, что если веб-ресурс включает вывод или функцию из других веб-ресурсов (например, страницу JSP, включающую вывод нескольких других страниц JSP), фильтры Servlet 2.4 могут работать до и после каждого из включенных ресурсов. .

Чтобы включить эту функцию, вам необходимо:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

RegistrationController

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
источник
Хорошая информация, но вводить имя пользователя и пароль в URL-адрес - это плохо. 1) экранирование не выполняется, поэтому имя пользователя или пароль со специальным символом, скорее всего, сломается или, что еще хуже, будет использоваться в качестве вектора эксплойта безопасности. 2) пароли в URL-адресах плохие, потому что URL-адреса часто записываются на диск, что очень плохо для безопасности - все ваши пароли в виде открытого текста просто сидят там.
goat
1

Я пытался протестировать приложение extjs, и после успешной установки testingAuthenticationToken оно внезапно перестало работать без очевидной причины.

Я не мог заставить приведенные выше ответы работать, поэтому моим решением было пропустить этот кусочек весны в тестовой среде. Я сделал такой шов вокруг пружины:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

Пользователь здесь - это настраиваемый тип.

Затем я помещаю его в класс, у которого просто есть возможность для тестового кода переключать пружину.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

Тестовая версия выглядит примерно так:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

В вызывающем коде я все еще использую правильного пользователя, загруженного из базы данных:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

Очевидно, это не подходит, если вам действительно нужно использовать безопасность, но я использую настройку без безопасности для тестового развертывания. Я думал, что кто-то другой может столкнуться с подобной ситуацией. Это шаблон, который я раньше использовал для имитации статических зависимостей. Другая альтернатива - вы можете поддерживать статичность класса-оболочки, но я предпочитаю этот, поскольку зависимости кода более явны, поскольку вам нужно передать CurrentUserAccessor в классы, где это требуется.

JonnyRaa
источник