如何获取活跃用户的用户详细信息

175

在我的控制器中,当我需要活跃(已登录)的用户时,我使用以下方法获取UserDetails实现:

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

它可以正常工作,但是我认为Spring在这种情况下可能会使生活更加轻松。是否有一种方法可以将UserDetails自动装配到控制器或方法中?

例如,类似于:

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

但我得到的是UserDetails,而不是UsernamePasswordAuthenticationToken

我正在寻找一种优雅的解决方案。有任何想法吗?

9个回答

232

前言:自从Spring-Security 3.2版本以后,有一个很好的注解@AuthenticationPrincipal,在本答案的末尾有描述。当您使用Spring-Security >= 3.2时,这是最好的选择。

当您:

  • 使用旧版本的Spring-Security,
  • 需要通过在主体中存储的某些信息(如登录或ID)从数据库加载自定义用户对象,或者
  • 想要了解HandlerMethodArgumentResolverWebArgumentResolver如何以优雅的方式解决此问题,或者只是想要了解@AuthenticationPrincipalAuthenticationPrincipalArgumentResolver背后的背景(因为它基于HandlerMethodArgumentResolver

那么请继续阅读——否则只需使用@AuthenticationPrincipal,并感谢Rob Winch(@AuthenticationPrincipal的作者)和Lukas Schmelzeisen(因他的回答)。

顺便提一句:我的回答有点旧了(2012年1月),所以基于Spring Security 3.2的@AuthenticationPrincipal注解解决方案中,Lukas Schmelzeisen是第一个提出来的。


然后您可以在控制器中使用。
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

当传入方法参数(第一个参数)时,该方法获取网络请求(第二个参数),并且如果它认为自己有责任处理该方法参数,则返回User
自Spring 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;
        }
   }
}

如果每个用户实例始终应从安全上下文中获取,但从未成为命令对象,则需要定义自定义注释。

@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。-请参见Jay的评论


对于Spring 3.1+,与HandlerMethodArgumentResolver相同

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 混淆)有自己的内置解决方案:@AuthenticationPrincipalorg.springframework.security.web.bind.annotation.AuthenticationPrincipal)。这在 Lukas Schmelzeisen`s answer 中有很好的描述。

只需要编写:

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

要使此功能正常工作,您需要注册AuthenticationPrincipalArgumentResolverorg.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver):可以通过“激活”@EnableWebMvcSecurity或在mvc:argument-resolvers中注册此bean来实现-与我上面描述的Spring 3.1解决方案相同。
请参见Spring Security 3.2参考手册,第11.2章@AuthenticationPrincipal

Spring-Security 4.0 解决方案

它的工作方式类似于Spring 3.2解决方案,但在Spring 4.0中,@AuthenticationPrincipalAuthenticationPrincipalArgumentResolver被“移动”到另一个包中:

但是旧包中的旧类仍然存在,所以不要混淆它们!
这只是写作。
import org.springframework.security.core.annotation.AuthenticationPrincipal;
ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
}

要使此功能正常工作,您需要注册 (org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver :通过“激活”@EnableWebMvcSecurity或在mvc:argument-resolvers中注册此bean - 与我上面描述的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


或者创建一个具有getUserDetails()方法的bean,并将其@Autowire到您的控制器中。 - sourcedelica
实现这个解决方案时,我在 resolveArgument() 的顶部设置了一个断点,但我的应用程序从未进入 Web 参数解析器。你的 Spring 配置是在 Servlet 上下文而不是根上下文中,对吧? - Jason
@Jay:这个配置是根上下文的一部分,而不是servlet上下文。-- 看起来我在示例中忘记指定id(id="applicationConversionService")了。 - Ralph
11
需要注意的是,如果您正在使用Spring 3.1,他们建议使用HandlerMethodArgumentResolver而不是WebArgumentResolver。我通过在servlet上下文中使用<annotation-driven>对HandlerMethodArgumentResolver进行配置来使其正常工作。除此之外,我按照这里发布的答案实现了它,一切工作得很好。 - Jason
@sourcedelica 为什么不直接创建静态方法呢? - Alex78191
显示剩余2条评论

67

虽然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
如何检查customUser是否为null? - Sajad
if (customUser != null) { ... } - T3rm1
如何使用Mockito模拟这个带注释的参数? - Avinash Gupta

27

Spring Security旨在与其他非Spring框架配合使用,因此它与Spring MVC的集成不太紧密。 Spring Security默认从HttpServletRequest.getUserPrincipal()方法返回Authentication对象,因此这就是您作为主体获取的内容。您可以直接从中获取您的UserDetails对象,方法如下:

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

请注意,对象类型可能会根据使用的身份验证机制而变化(例如,您可能不会得到UsernamePasswordAuthenticationToken),并且Authentication不一定必须包含UserDetails。它可以是字符串或任何其他类型。

如果您不想直接调用SecurityContextHolder,最优雅的方法(我会遵循的方法)是注入自己的自定义安全上下文访问器接口,并将其自定义以匹配您的需求和用户对象类型。创建一个带有相关方法的接口,例如:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

你可以通过访问SecurityContextHolder来实现这一点,在标准实现中,从而使你的代码完全脱离Spring Security。然后将其注入需要访问安全信息或当前用户信息的控制器。

另一个主要优点是,可以轻松地进行简单实现,并使用固定数据进行测试,无需担心填充线程本地变量等问题。


我曾考虑过这种方法,但我不确定a)如何以正确的方式执行它,b)是否会有任何线程问题。你确定那里不会有任何问题吗?我选择上面发布的注释方法,但我认为这仍然是一个不错的方法。感谢您的发布。 :) - The Awnry Bear
4
从技术上讲,这与直接从您的控制器访问SecurityContextHolder是相同的,因此不应该出现任何线程问题。它只是将调用保持在一个地方,并允许您轻松地注入替代项进行测试。您还可以在需要访问安全信息的其他非 Web 类中重复使用相同的方法。 - Shaun the Sheep
明白了...这正是我所想的。如果我在使用注解方法进行单元测试时遇到问题,或者想要减少与Spring的耦合,那么这将是一个很好的回头参考。 - The Awnry Bear
@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> - Eric
最好通过 spring-security-taglibs 在模板中获取已授权的用户:https://dev59.com/nmoy5IYBdhLWcg3wHabt#44373331 - Grigory Kislin

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";
   }
}

需要从 authentication.getPrincipal() 中分配 CustomUser。

以下是 AuthenticationPrincipalAuthenticationPrincipalArgumentResolver 的相应 Javadocs。


1
@nbro 当我添加了版本特定的解决方案时,其他解决方案都没有更新以考虑这个解决方案。 - geoand
AuthenticationPrincipalArgumentResolver 现已过时。 - Igor Donin

6
@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
要获取活跃用户的详细信息,您可以在控制器中使用@AuthenticationPrincipal,像这样:-
public String function(@AuthenticationPrincipal UserDetailsImpl user,
                    Model model){   
    model.addAttribute("username",user.getName()); //this user object 
contains user details 
    return "";
}

UserDetailsImpl.java

import com.zoom.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
public class UserDetailsImpl implements UserDetails {

@Autowired
private User user;

public UserDetailsImpl(User user) {
    this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ADMIN");
    return List.of(simpleGrantedAuthority);
}

@Override
public String getPassword() {
    return user.getPassword();
}

@Override
public String getUsername() {
    return user.getEmail();
}

@Override
public boolean isAccountNonExpired() {
    return true;
}

@Override
public boolean isAccountNonLocked() {
    return true;
}

@Override
public boolean isCredentialsNonExpired() {
    return true;
}

@Override
public boolean isEnabled() {
    return true;
}

public String getRole(){
    return user.getRole();
}

public String getName(){
    return user.getUsername();
}

public User getUser(){
    return user;
}
}

0
你可以尝试这样做: 通过使用Spring的Authentication Object,我们可以在控制器方法中获取用户详细信息。以下是示例,通过在控制器方法中传递Authentication对象以及参数。一旦用户经过身份验证,详细信息将填充在Authentication对象中。
@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}

请详细阐述您的答案。 - Nikolai Shevchenko
我已经编辑了我的回答,我们可以使用认证对象,该对象具有已经进行身份验证的用户的用户详细信息。 - Mirza Shujathullah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接