Как правильно получить информацию о текущем имени пользователя (т.е. SecurityContext) в компоненте при использовании Spring Security?

У меня есть веб-приложение Spring MVC, которое использует Spring Security. Я хочу знать имя пользователя, вошедшего в систему в данный момент. Я использую приведенный ниже фрагмент кода. Это приемлемый способ?

Мне не нравится вызов статического метода внутри этого контроллера - это побеждает всю цель Spring, ИМХО. Есть ли способ настроить приложение, чтобы вместо этого вводился текущий SecurityContext или текущая проверка подлинности?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }

Почему у вас нет контроллера (безопасного контроллера) в качестве суперкласса, чтобы получить пользователя из SecurityContext и установить его как переменную экземпляра внутри этого класса? Таким образом, когда вы расширяете безопасный контроллер, весь ваш класс будет иметь доступ к принципалу User текущего контекста.

Dehan 11.11.2017 08:34
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
297
1
174 902
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

Для последнего написанного мной приложения Spring MVC я не вводил держатель SecurityContext, но у меня был базовый контроллер, с которым у меня было два служебных метода, связанных с этим ... isAuthenticated () и getUsername (). Внутри они выполняют описанный вами статический вызов метода.

По крайней мере, тогда это будет только один раз, если вам понадобится позже рефакторинг.

Вы можете использовать Spring AOP aproach. Например, если у вас есть служба, которой необходимо знать текущего участника. Вы можете ввести пользовательскую аннотацию, например @Principal, которая указывает на то, что эта служба должна зависеть от принципала.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Затем в вашем совете, который, как мне кажется, необходимо расширить MethodBeforeAdvice, проверьте, что эта конкретная служба имеет аннотацию @Principal и введите имя участника или вместо этого установите для нее значение «АНОНИМНЫЙ».

Мне нужен доступ к принципалу внутри класса обслуживания, не могли бы вы опубликовать полный пример на github? Я не знаю Spring АОП, отсюда и просьба.

Rakesh Waghela 24.07.2013 16:49

Единственная проблема заключается в том, что даже после аутентификации с помощью Spring Security компонент пользователя / участника не существует в контейнере, поэтому внедрение зависимостей будет затруднено. Перед тем, как использовать Spring Security, мы должны были создать bean-компонент с ограниченным сеансом, у которого был текущий участник, внедрить его в «AuthService», а затем внедрить этот сервис в большинство других сервисов в приложении. Таким образом, эти Службы просто вызовут authService.getCurrentUser (), чтобы получить объект. Если у вас есть место в вашем коде, где вы получаете ссылку на того же участника в сеансе, вы можете просто установить его как свойство для вашего bean-компонента с областью действия сеанса.

Я согласен с тем, что запрашивать SecurityContext для текущего пользователя воняет, это кажется очень не-Spring способом справиться с этой проблемой.

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

/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
* 
* @return User object for the currently logged in user, or null if no User
*         is logged in.
*/
public static User getCurrentUser() {

    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()

    if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();

    // principal object is either null or represents anonymous user -
    // neither of which our domain User object can represent - so return null
    return null;
}


/**
 * Utility method to determine if the current user is logged in /
 * authenticated.
 * <p>
 * Equivalent of calling:
 * <p>
 * <code>getCurrentUser() != null</code>
 * 
 * @return if user is logged in
 */
public static boolean isLoggedIn() {
    return getCurrentUser() != null;
}

он имеет длину, равную SecurityContextHolder.getContext (), и последний является потокобезопасным, поскольку сохраняет детали безопасности в threadLocal. Этот код не поддерживает состояние.

matt b 29.05.2010 07:43

С тех пор, как был дан ответ на этот вопрос, в мире Spring многое изменилось. Spring упростил получение текущего пользователя в контроллере. Для других bean-компонентов Spring принял предложения автора и упростил внедрение SecurityContextHolder. Подробности в комментариях.


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

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Теперь мой контроллер (или любой другой POJO) будет выглядеть так:

public class FooController {

  private final SecurityContextFacade securityContextFacade;

  public FooController(SecurityContextFacade securityContextFacade) {
    this.securityContextFacade = securityContextFacade;
  }

  public void doSomething(){
    SecurityContext context = securityContextFacade.getContext();
    // do something w/ context
  }

}

А поскольку интерфейс является точкой разделения, модульное тестирование не вызывает затруднений. В этом примере я использую Mockito:

public class FooControllerTest {

  private FooController controller;
  private SecurityContextFacade mockSecurityContextFacade;
  private SecurityContext mockSecurityContext;

  @Before
  public void setUp() throws Exception {
    mockSecurityContextFacade = mock(SecurityContextFacade.class);
    mockSecurityContext = mock(SecurityContext.class);
    stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
    controller = new FooController(mockSecurityContextFacade);
  }

  @Test
  public void testDoSomething() {
    controller.doSomething();
    verify(mockSecurityContextFacade).getContext();
  }

}

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

public class SecurityContextHolderFacade implements SecurityContextFacade {

  public SecurityContext getContext() {
    return SecurityContextHolder.getContext();
  }

  public void setContext(SecurityContext securityContext) {
    SecurityContextHolder.setContext(securityContext);
  }

}

И, наконец, производственная конфигурация Spring выглядит так:

<bean id = "myController" class = "com.foo.FooController">
     ...
  <constructor-arg index = "1">
    <bean class = "com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Кажется более чем немного глупым, что Spring, контейнер для внедрения зависимостей всего, не предоставил способ внедрить что-то подобное. Я так понимаю, SecurityContextHolder унаследован от acegi, но все же. Дело в том, что они так близки - если бы только у SecurityContextHolder был геттер для получения базового экземпляра SecurityContextHolderStrategy (который является интерфейсом), вы могли бы внедрить его. Фактически, я даже открыл выпуск Jira на этот счет.

И последнее - я только что существенно изменил ответ, который у меня был здесь раньше. Если вам интересно, проверьте историю, но, как мне заметил коллега, мой предыдущий ответ не работал бы в многопоточной среде. Базовый SecurityContextHolderStrategy, используемый SecurityContextHolder, по умолчанию является экземпляром ThreadLocalSecurityContextHolderStrategy, который хранит SecurityContext в ThreadLocal. Следовательно, не обязательно вводить SecurityContext непосредственно в компонент во время инициализации - его может потребоваться каждый раз извлекать из ThreadLocal в многопоточной среде, чтобы был извлечен правильный.

Мне нравится ваше решение - это умное использование поддержки фабричных методов в Spring. Тем не менее, это работает для вас, потому что объект контроллера привязан к веб-запросу. Если вы неправильно измените область действия bean-компонента контроллера, это может сломаться.

Paul Morie 20.05.2009 01:48

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

Scott Bale 12.07.2009 07:13

Это все еще рекомендуемое решение в текущем выпуске Spring? Я не могу поверить, что ему нужно столько кода, чтобы получить только имя пользователя.

Ta Sas 03.08.2010 22:42

Если вы используете Spring Security 3.0.x, они реализовали мое предложение в проблеме JIRA. Я зарегистрировал jira.springsource.org/browse/SEC-1188, поэтому теперь вы можете вставить экземпляр SecurityContextHolderStrategy (из SecurityContextHolder) непосредственно в свой bean-компонент через стандартную конфигурацию Spring.

Scott Bale 04.08.2010 18:09

См. Ответ tsunade21. Spring 3 теперь позволяет использовать java.security.Principal в качестве аргумента метода в вашем контроллере.

Patrick 16.06.2011 21:46

Да, статика в целом плохая, но в этом случае статика - это наиболее безопасный код, который вы можете написать. Поскольку контекст безопасности связывает участника с текущим запущенным потоком, наиболее безопасный код будет обращаться к статике из потока как можно напрямую. Скрытие доступа за внедренным классом-оболочкой дает злоумышленнику больше точек для атаки. Им не понадобится доступ к коду (который им было бы трудно изменить, если бы jar был подписан), им просто нужен способ переопределить конфигурацию, что можно сделать во время выполнения или вставить некоторый XML в путь к классам. Даже использование инъекции аннотации в подписанный код можно переопределить с помощью внешнего XML. Такой XML может внедрить в работающую систему мошенника. Вероятно, поэтому Spring в данном случае делает что-то не похожее на Spring.

Я бы просто сделал это:

request.getRemoteUser();

Это может сработать, но ненадежно. Из javadoc: «Отправляется ли имя пользователя с каждым последующим запросом, зависит от браузера и типа аутентификации». -download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…

Scott Bale 11.08.2010 01:31

На самом деле это действительный и очень простой способ получить удаленное имя пользователя в веб-приложении Spring Security. Стандартная цепочка фильтров включает SecurityContextHolderAwareRequestFilter, который обертывает запрос и реализует этот вызов, обращаясь к SecurityContextHolder.

Shaun the Sheep 03.01.2012 02:00

Я получаю аутентифицированного пользователя HttpServletRequest.getUserPrincipal ();

Пример:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;

import foo.Form;

@Controller
@RequestMapping(value = "/welcome")
public class IndexController {

    @RequestMapping(method=RequestMethod.GET)
    public String getCreateForm(Model model, HttpServletRequest request) {

        if (request.getUserPrincipal() != null) {
            String loginName = request.getUserPrincipal().getName();
            System.out.println("loginName : " + loginName );
        }

        model.addAttribute("form", new Form());
        return "welcome";
    }
}

Мне нравится Ваше решение. Специалистам Spring: это хорошее и надежное решение?

marioosh 12.04.2011 12:15

Не лучшее решение. Вы получите null, если пользователь аутентифицирован анонимно (элементы http> anonymous в Spring Security XML). SecurityContextHolder или SecurityContextHolderStrategy - правильный путь.

Nowaker 04.07.2011 05:47

Итак, я проверил, не равен ли null request.getUserPrincipal ()! = Null.

digz6666 22.08.2011 09:44

Нулевое значение в фильтре

Alex78191 22.05.2018 04:17
Ответ принят как подходящий

Если вы используете Spring 3, самый простой способ:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }

Попробуй это

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();

Вызов статического метода SecurityContextHolder.getContext () - это именно то, на что я жаловался в исходном вопросе. Вы ничего не ответили.

Scott Bale 01.12.2011 00:51

Однако это именно то, что рекомендует документация: static.springsource.org/spring-security/site/docs/3.0.x/… Итак, чего вы добиваетесь, избегая этого? Вы ищете сложное решение простой проблемы. В лучшем случае - вы получите такое же поведение. В худшем случае вы получите ошибку или дыру в безопасности.

Bob Kerns 23.09.2012 11:41

@BobKerns Для тестирования было бы проще внедрить аутентификацию, а не помещать ее в локальный поток.

user41871 09.07.2013 03:13

Лучшее решение, если вы используете Spring 3 и вам нужен аутентифицированный участник в вашем контроллере, - это сделать что-то вроде этого:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;

    @Controller
    public class KnoteController {
        @RequestMapping(method = RequestMethod.GET)
        public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {

            if (authToken instanceof UsernamePasswordAuthenticationToken) {
                user = (User) authToken.getPrincipal();
            }
            ...

    }

Почему вы выполняете проверку экземпляра UsernamePasswordAuthenticationToken, когда параметр уже имеет тип UsernamePasswordAuthenticationToken?

Scott Bale 14.12.2011 23:34

(authToken instanceof UsernamePasswordAuthenticationToken) является функциональным эквивалентом if (authToken! = null). Последний может быть немного чище, но в остальном разницы нет.

Mark 16.12.2011 11:15

Чтобы он просто отображался на ваших JSP-страницах, вы можете использовать Spring Security Tag Lib:

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

Чтобы использовать любой из тегов, в вашем JSP должна быть объявлена ​​библиотека тегов безопасности:

<%@ taglib prefix = "security" uri = "http://www.springframework.org/security/tags" %>

Затем на странице jsp сделайте что-то вроде этого:

<security:authorize access = "isAuthenticated()">
    logged in as <security:authentication property = "principal.username" /> 
</security:authorize>

<security:authorize access = "! isAuthenticated()">
    not logged in
</security:authorize>

ПРИМЕЧАНИЕ. Как упоминалось в комментариях @ SBerg413, вам необходимо добавить

use-expressions = "true"

к тегу "http" в конфигурации security.xml, чтобы это работало.

Похоже, это, вероятно, одобренный Spring Security способ!

Nick Spacek 16.08.2012 20:04

Чтобы этот метод работал, вам необходимо добавить use-expression = "true" в тег http в конфигурации security.xml.

SBerg413 01.03.2013 23:49

Спасибо @ SBerg413, я отредактирую свой ответ и добавлю ваше важное уточнение!

Brad Parks 02.03.2013 00:40

Мне нравится делиться своим способом поддержки информации о пользователях на странице freemarker. Все очень просто и отлично работает!

Вам просто нужно разместить повторный запрос аутентификации на default-target-url (страница после входа в форму) Это мой метод Controler для этой страницы:

@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
    showRequestLog("monitoring");


    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    //create a new session
    HttpSession session = request.getSession(true);
    session.setAttribute("username", userName);

    return new ModelAndView(catalogPath + "monitoring");
}

А это мой ftl-код:

<@security.authorize ifAnyGranted = "ROLE_ADMIN, ROLE_USER">
<p style = "padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize> 

И все, имя пользователя будет отображаться на каждой странице после авторизации.

Спасибо за попытку ответить, но использование статического метода SecurityContextHolder.getContext () - это именно то, чего я хотел избежать, и это причина, по которой я задал этот вопрос в первую очередь.

Scott Bale 05.07.2013 01:34

В Spring 3+ у вас есть следующие возможности.

Опция 1 :

@RequestMapping(method = RequestMethod.GET)    
public String currentUserNameByPrincipal(Principal principal) {
    return principal.getName();
}

Вариант 2:

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
    return authentication.getName();
}

Вариант 3:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserByHTTPRequest(HttpServletRequest request) {
    return request.getUserPrincipal().getName();

}

Вариант 4: Необычный: Проверьте это для получения более подробной информации

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}

Начиная с версии 3.2, spring-security-web поставляется с @CurrentUser, который работает как пользовательский @ActiveUser из вашей ссылки.

Mike Partridge 18.07.2014 00:14

@MikePartridge, кажется, я не нашел, что вы говорите, ни одной ссылки ?? или подробнее ??

azerafati 31.01.2015 18:07

Моя ошибка - я неправильно понял javadoc Spring Security AuthenticationPrincipalArgumentResolver. Там показан пример упаковки @AuthenticationPrincipal с пользовательской аннотацией @CurrentUser. Начиная с версии 3.2 нам не нужно реализовывать настраиваемый преобразователь аргументов, как в связанном ответе. Этот другой ответ имеет более подробную информацию.

Mike Partridge 02.02.2015 16:32

Я использую аннотацию @AuthenticationPrincipal в классах @Controller, а также в аннотированных классах @ControllerAdvicer. Бывший.:

@ControllerAdvice
public class ControllerAdvicer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);


    @ModelAttribute("userActive")
    public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
    {
        return currentUser;
    }
}

Где UserActive - это класс, который я использую для служб зарегистрированных пользователей, и расширяется от org.springframework.security.core.userdetails.User. Что-то типа:

public class UserActive extends org.springframework.security.core.userdetails.User
{

    private final User user;

    public UserActive(User user)
    {
        super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
        this.user = user;
    }

     //More functions
}

Действительно просто.

Определите Principal как зависимость в методе вашего контроллера, и spring внедрит текущего аутентифицированного пользователя в ваш метод при вызове.

Если вы используете Spring Security ver> = 3.2, вы можете использовать аннотацию @AuthenticationPrincipal:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
    String currentUsername = currentUser.getUsername();
    // ...
}

Здесь CustomUser - это настраиваемый объект, реализующий UserDetails, который возвращается настраиваемым UserDetailsService.

Дополнительную информацию можно найти в главе @AuthenticationPrincipal справочной документации Spring Security.

Другие вопросы по теме