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


288

У меня есть веб-приложение 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 текущего контекста.
Дехан де Кроос

Ответы:


259

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

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

     final String currentUser = principal.getName();

 }

69

С тех пор, как на этот вопрос был дан ответ, многое изменилось в мире весны. 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, который хранит SecurityContexts в ThreadLocal. Следовательно, не обязательно хорошая идея вставлять SecurityContextобъект непосредственно в bean-компонент во время инициализации - его может потребоваться извлекать ThreadLocalкаждый раз, в многопоточной среде, так что получается правильный.


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

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

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

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

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

22

Я согласен с тем, что необходимость запрашивать 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;
}

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

22

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

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

Чтобы использовать любой из тегов, у вас должен быть тег безопасности liblib, объявленный в вашем 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, вам необходимо добавить

потребительные выражения = «истина»

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


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

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

Спасибо @ SBerg413, я отредактирую свой ответ и добавлю ваши важные разъяснения!
Брэд Паркс

14

Если вы используете 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.


13

Я получаю аутентифицированного пользователя по 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: это безопасное, хорошее решение?
Мариоош

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

1
Так что я проверил, если не null request.getUserPrincipal ()! = Null.
digz6666


9

В 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) {
  ...
}

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

@MikePartridge, я не вижу, что вы говорите, какая-либо ссылка? или больше информации ??
Азерафати

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

5

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


5

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

request.getRemoteUser();

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

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

4

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

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


3

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

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

Затем в своем совете, который, по моему мнению, должен расширять MethodBeforeAdvice, убедитесь, что конкретный сервис имеет аннотацию @Principal, и введите имя Принципала или установите для него значение «ANONYMOUS».


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

2

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


1

Попробуй это

Аутентификация аутентификации = SecurityContextHolder.getContext (). GetAuthentication ();
Строка userName = authentication.getName ();


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

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

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

1

Лучшее решение, если вы используете 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();
            }
            ...

    }

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

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

1

Я использую @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
}

Действительно легко.


0

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


-2

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

Вам просто нужно разместить запрос на аутентификацию 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 () - это именно то, чего я хотел избежать, и причина, по которой я задал этот вопрос в первую очередь.
Скотт Бэйл
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.