Как получить UserDetails активного пользователя

170

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

User activeUser = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.debug(activeUser.getSomeCustomField());

Это прекрасно работает, но я думаю, что Spring может облегчить жизнь в таком случае, как этот. Есть ли способ UserDetailsвключить автопроводку в контроллер или в метод?

Например, что-то вроде:

public ModelAndView someRequestHandler(Principal principal) { ... }

Но вместо того, чтобы получить UsernamePasswordAuthenticationToken, я получаю UserDetailsвместо?

Я ищу элегантное решение. Любые идеи?

Медведь Ошри
источник

Ответы:

226

Преамбула: Начиная с Spring-Security 3.2, @AuthenticationPrincipalв конце этого ответа есть хорошая аннотация . Это лучший способ, когда вы используете Spring-Security> = 3.2.

Когда ты:

  • использовать старую версию Spring-Security,
  • необходимо загрузить пользовательский объект пользователя из базы данных с помощью некоторой информации (например, имени входа или идентификатора), хранящейся в основной или
  • хотите узнать, как HandlerMethodArgumentResolverили WebArgumentResolverможет решить эту проблему элегантным способом, или просто хотите изучить фон @AuthenticationPrincipalи AuthenticationPrincipalArgumentResolver(потому что он основан на HandlerMethodArgumentResolver)

затем продолжайте читать - иначе просто используйте @AuthenticationPrincipalи поблагодарите Роба Винча (Автор @AuthenticationPrincipal) и Лукаса Шмельцайзена (за его ответ).

(Кстати: мой ответ немного старше (январь 2012 г.), поэтому именно Лукас Шмельцайзен выступил первым с @AuthenticationPrincipalбазой решений для аннотаций в Spring Security 3.2.)


Тогда вы можете использовать в своем контроллере

public ModelAndView someRequestHandler(Principal principal) {
   User activeUser = (User) ((Authentication) principal).getPrincipal();
   ...
}

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

Так что вы действительно хотите иметь такой контроллер:

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

Поэтому вам нужно только реализовать WebArgumentResolver. У него есть метод

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Он получает веб-запрос (второй параметр) и должен возвращать Userif, если он чувствует себя ответственным за аргумент метода (первый параметр).

С весны 3.1 появилась новая концепция HandlerMethodArgumentResolver. Если вы используете Spring 3.1+, то вам следует использовать его. (Это описано в следующем разделе этого ответа))

public class CurrentUserWebArgumentResolver implements WebArgumentResolver{

   Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) {
        if(methodParameter is for type User && methodParameter is annotated with @ActiveUser) {
           Principal principal = webRequest.getUserPrincipal();
           return (User) ((Authentication) principal).getPrincipal();
        } else {
           return WebArgumentResolver.UNRESOLVED;
        }
   }
}

Вам необходимо определить пользовательскую аннотацию - вы можете пропустить ее, если каждый экземпляр User всегда должен быть взят из контекста безопасности, но никогда не является объектом команды.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActiveUser {}

В конфигурации вам нужно только добавить это:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"
    id="applicationConversionService">
    <property name="customArgumentResolver">
        <bean class="CurrentUserWebArgumentResolver"/>
    </property>
</bean>

@ См. Научитесь настраивать аргументы метода Spring MVC @Controller.

Следует отметить, что если вы используете Spring 3.1, они рекомендуют HandlerMethodArgumentResolver вместо WebArgumentResolver. - см. комментарий от Джея


То же самое с HandlerMethodArgumentResolverдля весны 3.1+

public class CurrentUserHandlerMethodArgumentResolver
                               implements HandlerMethodArgumentResolver {

     @Override
     public boolean supportsParameter(MethodParameter methodParameter) {
          return
              methodParameter.getParameterAnnotation(ActiveUser.class) != null
              && methodParameter.getParameterType().equals(User.class);
     }

     @Override
     public Object resolveArgument(MethodParameter methodParameter,
                         ModelAndViewContainer mavContainer,
                         NativeWebRequest webRequest,
                         WebDataBinderFactory binderFactory) throws Exception {

          if (this.supportsParameter(methodParameter)) {
              Principal principal = webRequest.getUserPrincipal();
              return (User) ((Authentication) principal).getPrincipal();
          } else {
              return WebArgumentResolver.UNRESOLVED;
          }
     }
}

В настройку нужно добавить это

<mvc:annotation-driven>
      <mvc:argument-resolvers>
           <bean class="CurrentUserHandlerMethodArgumentResolver"/>         
      </mvc:argument-resolvers>
 </mvc:annotation-driven>

@ См. Использование интерфейса Spring MVC 3.1 HandlerMethodArgumentResolver


Решение Spring-Security 3.2

Spring Security 3.2 (не путайте с Spring 3.2) имеет собственное встроенное решение: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Это хорошо описано в ответе Лукаса Шмельцайзена

Просто пишу

ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
 }

Чтобы это работало, вам нужно зарегистрировать AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): либо «активировать», @EnableWebMvcSecurityлибо зарегистрировать этот bean-компонент внутри mvc:argument-resolvers- так же, как я описал это с помощью решения Spring 3.1 выше.

@ См. Spring Security 3.2. Ссылка, глава 11.2. @AuthenticationPrincipal


Решение Spring-Security 4.0

Он работает как 3.2 решения Spring, но весной 4.0, @AuthenticationPrincipalи AuthenticationPrincipalArgumentResolverбыл «перемещен» на другой пакет:

(Но старые классы в старых пакетах все еще существуют, поэтому не смешивайте их!)

Просто пишу

import org.springframework.security.core.annotation.AuthenticationPrincipal;
ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
}

Чтобы это работало, вам нужно зарегистрировать ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: либо «активировать», @EnableWebMvcSecurityлибо зарегистрировать этот bean-компонент внутри mvc:argument-resolvers- так же, как я описал это с помощью решения Spring 3.1 выше.

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
    </mvc:argument-resolvers>
</mvc:annotation-driven>

@ См. Spring Security 5.0 Справочник, Глава 39.3 @AuthenticationPrincipal

Ральф
источник
В качестве альтернативы просто создайте bean-компонент, который имеет метод getUserDetails () и @Autowire, в ваш контроллер.
sourcedelica
Реализуя это решение, я установил точку останова в верхней части resolArgument (), но мое приложение никогда не входит в распознаватель веб-аргументов. Ваша весенняя конфигурация в контексте сервлета, а не корневой контекст, верно?
Джей
@Jay: эта конфигурация является частью корневого контекста, а не контекста сервлета. - это кажется, что я забыл указать идентификатор (id="applicationConversionService")в примере
Ральф
11
Следует отметить, что если вы используете Spring 3.1, они рекомендуют HandlerMethodArgumentResolver вместо WebArgumentResolver. Я заставил HandlerMethodArgumentResolver работать, настроив его с помощью <annotation-driven> в контексте сервлета. Кроме этого, я реализовал ответ, как здесь опубликовано, и все отлично работает
Jay
@sourcedelica Почему бы просто не создать статический метод?
Alex78191
66

В то время как Ralphs Answer предоставляет элегантное решение, в Spring Security 3.2 вам больше не нужно внедрять свои собственные ArgumentResolver.

Если у вас есть UserDetailsреализация CustomUser, вы можете просто сделать это:

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

    // .. find messages for this User and return them...
}

См. Документацию Spring Security: @AuthenticationPrincipal

Лукас Шмельцайзен
источник
2
для тех, кто не любит читать предоставленные ссылки, это должно быть включено @EnableWebMvcSecurityили в XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik
Как проверить, является ли значение customUsernull или нет?
Саджад
if (customUser != null) { ... }
T3rm1
27

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

UserDetails ud = ((Authentication)principal).getPrincipal()

Также обратите внимание, что типы объектов могут различаться в зависимости от используемого механизма аутентификации (например, вы можете не получить a UsernamePasswordAuthenticationToken), и Authenticationон не обязательно должен содержать a UserDetails. Это может быть строка или любой другой тип.

Если вы не хотите звонить SecurityContextHolderнапрямую, самый элегантный подход (которому я бы следовал) - внедрить свой собственный интерфейс доступа к контексту безопасности, настроенный в соответствии с вашими потребностями и типами пользовательских объектов. Создайте интерфейс с соответствующими методами, например:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Затем вы можете реализовать это, обратившись к SecurityContextHolderстандартной реализации, таким образом полностью отделив свой код от Spring Security. Затем введите это в контроллеры, которым необходим доступ к информации о безопасности или информации о текущем пользователе.

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

Шон Овца
источник
Я рассматривал этот подход, но я не был уверен, а) как именно это сделать правильно (тм) и б), если возникнут какие-либо проблемы с потоками. Вы уверены, что там не будет проблем? Я собираюсь использовать метод аннотации, опубликованный выше, но я думаю, что это все еще достойный способ сделать это. Спасибо за публикацию. :)
Awnry Bear
4
Технически это то же самое, что и доступ к SecurityContextHolder напрямую с вашего контроллера, поэтому не должно быть никаких проблем с потоками. Он просто удерживает вызов в одном месте и позволяет легко вводить альтернативы для тестирования. Вы также можете повторно использовать тот же подход в других не веб-классах, которым требуется доступ к информации о безопасности.
Шон Овца
Понял ... это то, что я думал. К этому вопросу можно вернуться, если у меня возникнут проблемы с модульным тестированием с помощью метода аннотаций или если я захочу уменьшить связь с Spring.
Медведь Ошри
@LukeTaylor, не могли бы вы подробнее объяснить этот подход, я немного новичок в Spring и Spring Security, так что я не совсем понимаю, как это реализовать. Где я могу реализовать это, чтобы к нему можно было получить доступ? на мой UserServiceImpl?
Cu7l4ss
9

Реализуйте HandlerInterceptorинтерфейс, а затем вставьте UserDetailsв каждый запрос, который имеет модель, следующим образом:

@Component 
public class UserInterceptor implements HandlerInterceptor {
    ....other methods not shown....
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if(modelAndView != null){
            modelAndView.addObject("user", (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        }
}
поезд
источник
1
Спасибо @atrain, это полезно и элегантно. Кроме того, мне пришлось добавить <mvc:interceptors>в мой файл конфигурации приложения.
Эрик
Лучше авторизовать пользователя в шаблоне через spring-security-taglibs: stackoverflow.com/a/44373331/548473
Григорий Кислин
9

Начиная с версии Spring Security 3.2, пользовательские функции, реализованные в некоторых предыдущих ответах, существуют сразу после установки в виде @AuthenticationPrincipalаннотации, которая поддерживаетсяAuthenticationPrincipalArgumentResolver .

Простой пример его использования:

@Controller
public class MyController {
   @RequestMapping("/user/current/show")
   public String show(@AuthenticationPrincipal CustomUser customUser) {
        // do something with CustomUser
       return "view";
   }
}

CustomUser должен быть назначен из authentication.getPrincipal()

Вот соответствующие Javadocs AuthenticationPrincipal и AuthenticationPrincipalArgumentResolver

geoand
источник
1
@nbro Когда я добавил решение для конкретной версии, ни одно из других решений не было обновлено, чтобы учесть это решение
geoand
AuthenticationPrincipalArgumentResolver устарела
Игорь Донин
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Товарищ
источник
0

И если вам нужен авторизованный пользователь в шаблонах (например, JSP), используйте

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authentication property="principal.yourCustomField"/>

вместе с

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring-security.version}</version>
    </dependency>
Григорий Кислин
источник
0

Вы можете попробовать это: Используя Authentication Object из Spring, мы можем получить от него данные о пользователе в методе контроллера. Ниже приведен пример, передавая объект Authentication в методе контроллера вместе с аргументом. Когда пользователь проходит аутентификацию, детали заполняются в объекте Authentication.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Мирза Шуджатулла
источник
Пожалуйста, опишите ваш ответ.
Николай Шевченко
Я отредактировал свой ответ, мы могли бы использовать объект аутентификации, в котором есть данные о пользователе, который уже прошел аутентификацию.
Мирза Шуджатулла