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

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

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

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

17 ответов


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

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

     final String currentUser = principal.getName();

 }

многое изменилось в весеннем мире с тех пор, как на этот вопрос был дан ответ. 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);
  }

}

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

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

кажется более чем глупым, что Spring, контейнер для инъекций зависимостей всех вещей, не предоставил способ ввести что-то подобное. Я понимаю!--5--> был унаследован от acegi, но все же. Дело в том, что они так близко - если бы только!--5--> был геттер, чтобы получить основной SecurityContextHolderStrategy экземпляр (который является интерфейсом), вы можете ввести это. На самом деле, я даже открыл выпуск Jira об этом.

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


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

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

/**
* 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;
}

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

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

чтобы использовать любой из тегов, вы должны иметь taglib безопасности, объявленный в вашем 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" в безопасности.XML config для этого работать.


Я получаю аутентифицированного пользователя 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 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.


весной 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: Fancy one:проверьте это для более подробной информации

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

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


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

request.getRemoteUser();

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

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


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

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

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


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


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

    }

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

очень легко.


попробуй такое

проверка подлинности проверка подлинности = SecurityContextHolder.метода getcontext().getAuthentication ();
String userName = аутентификация.getName ();


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


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

вы просто должны разместить rerequest проверки подлинности на 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");
}

и это мой сверхсветовой код:

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

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