Як вручну встановити автентифікованого користувача у Spring Security / SpringMVC


107

Після того, як новий користувач подає форму "Новий обліковий запис", я хочу вручну ввійти до нього, щоб їм не довелося входити на наступну сторінку.

Сторінка входу в звичайну форму, що проходить через весняний перехоплювач безпеки, працює просто чудово.

У контролері форми нового облікового запису я створюю 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/…
коза

Відповіді:


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
Чи можете ви поясніть, як ви отримуєте автентифікаціюProvider
Vivex

1
@ s1moner3d вам слід отримати можливість вводити його через IoC -> \ @Autowired
Hartmut

1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi

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

12
звідки ти берешся authenticationManager?
Ісаак

2
authenticationManager в автоматичному режимі у вашому класі, наприклад, цей @Autowired AuthenticationServiceImpl authenticationManager. І також має бути ін'єкція квасолі у вашій конфігурації xml, тож Весна знає, що робити.

1
де реалізація AuthenticationServiceImpl? Що тримає цей клас?
Pra_A

3
Чому потрібно створити новий сеанс? Це SecurityContext не справляється з цим?
Влад Мануель Муреджан

17

Зрештою з'ясував корінь проблеми.

Коли я створюю контекст безпеки вручну, об’єкт сеансу не створюється. Тільки коли запит закінчує обробку, механізм Spring Security усвідомлює, що об’єкт сеансу є нульовим (коли він намагається зберігати контекст безпеки до сеансу після обробки запиту).

В кінці запиту Spring Security створює новий об’єкт сеансу та ідентифікатор сесії. Однак цей новий ідентифікатор сеансу ніколи не робить його веб-переглядачем, оскільки він відбувається в кінці запиту, після того, як відповідь на браузер була зроблена. Це призводить до того, що новий ідентифікатор сеансу (а отже, і контекст безпеки, що містить мій користувач, зареєстрований вручну), втрачається, коли наступний запит містить ідентифікатор попереднього сеансу.


4
Чесно кажучи, це відчуває як недоліки дизайну у захисті весни більше, ніж усе. Існує безліч фреймворків, написаних іншими мовами, які б з цим не мали проблем, але Spring Security просто порушується.
кругляки

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>

РеєстраціяКонтролер

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();

Хороша інформація, але введення імені користувача та пароля в URL-адресу погано. 1) не виконуватись ніяких епізодів, тому ім'я користувача або пароль із спеціальним символом, ймовірно, зламаються, або ще гірше, використовуються як вектор експлуатації безпеки. 2) паролі в URL-адресах погані, оскільки URL-адреси часто потрапляють на диск, що дуже погано для безпеки - всі ваші паролі в простому тексті просто сидять там.
коза

1

Я намагався протестувати додаток extjs і після успішного встановлення тестуванняAuthenticationToken це раптом перестало працювати без очевидних причин.

Я не міг отримати вищезазначені відповіді для роботи, тому моїм рішенням було пропустити цю частину весни в тестовому середовищі. Я ввела шов навколо весни так:

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 в класи, де це потрібно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.