在使用Spring Security时,如何以适当的方式获取bean中当前用户名(即SecurityContext)信息?

305

我有一个使用Spring Security的Spring MVC Web应用程序。 我想知道当前登录用户的用户名。 我正在使用以下代码片段。 这是被接受的方式吗?

我不喜欢在控制器内调用静态方法-这破坏了Spring的整个目的,依我看来。 是否有一种方法可以配置应用程序以注入当前SecurityContext或当前Authentication?

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

为什么不将控制器(安全控制器)作为超类,从SecurityContext获取用户并将其设置为该类内的实例变量?这样,当您扩展安全控制器时,整个类都将可以访问当前上下文的用户主体。 - Dehan
17个回答

273
如果您正在使用Spring 3,最简单的方法是:
 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }

71
这个问题被回答后,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实例(它是一个接口)的getter方法就可以进行注入。事实上,我甚至提出了一个 Jira 问题

最后一件事 - 我刚刚大幅修改了之前的答案。如果你感兴趣,可以查看历史记录,但正如一位同事指出的那样,我的之前的答案在多线程环境下无法工作。由SecurityContextHolder使用的底层SecurityContextHolderStrategy默认情况下是ThreadLocalSecurityContextHolderStrategy的一个实例,它将SecurityContext存储在ThreadLocal中。因此,在初始化时直接将SecurityContext注入到bean中可能不是一个好主意 - 在多线程环境中,每次需要从ThreadLocal中检索,以便正确地检索到它。


1
我喜欢你的解决方案——这是在Spring中使用工厂方法支持的巧妙运用。话虽如此,这个方案之所以对你有效,是因为控制器对象的作用域被限定在Web请求中。如果你错误地改变了控制器bean的作用域,那么这个方案就会失效。 - Paul Morie
2
前面两条评论是针对一个旧的、不正确的答案,我已经刚刚替换了它。 - Scott Bale
14
这在当前的Spring版本中仍然是推荐的解决方案吗?我无法相信只需检索用户名就需要这么多代码。 - Ta Sas
7
如果你正在使用 Spring Security 3.0.x 版本,他们已经在我提出的 JIRA 问题 https://jira.springsource.org/browse/SEC-1188 中实现了我的建议,因此你现在可以通过标准的 Spring 配置直接将 SecurityContextHolderStrategy 实例(来自 SecurityContextHolder)注入到你的 bean 中。 - Scott Bale
4
请参考tsunade21的回答。Spring 3现在允许您在控制器中将java.security.Principal作为方法参数使用。 - Patrick

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

23
这段代码的长度与SecurityContextHolder.getContext()方法长度相同,而且该方法是线程安全的,因为它将安全细节存储在ThreadLocal中。这段代码不维护任何状态。 - matt b

22
为了让它只在您的JSP页面中显示,您可以使用Spring安全标签库:

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

要使用任何标签,您必须在JSP中声明security taglib:
<%@ 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"

添加到security.xml配置文件中的“http”标签中才能使其起作用。

这似乎是Spring Security认可的方式! - Nick Spacek
3
为了让这种方法起作用,您需要在security.xml配置文件中的http标签中添加use-expressions="true"。 - SBerg413
感谢 @SBerg413,我会编辑我的答案并加上你的重要澄清! - Brad Parks

16

如果您正在使用Spring Security版本>=3.2,您可以使用@AuthenticationPrincipal注解:

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

这里的CustomUser是一个自定义对象,实现了UserDetails接口,并由自定义的UserDetailsService返回。

更多信息可以在Spring Security参考文档的@AuthenticationPrincipal章节中找到。


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专家来说,这是一个安全、好的解决方案吗? - marioosh
不是一个好的解决方案。如果用户是匿名身份验证(Spring Security XML中的http>anonymous元素),则会得到nullSecurityContextHolderSecurityContextHolderStrategy是正确的方式。 - Nowaker
1
所以我已经检查了如果不是空的请求.getUserPrincipal() != null。 - digz6666
在过滤器中是空的 - Alex78191

10
在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
自3.2版本开始,spring-security-web带有@CurrentUser注解,其作用类似于您链接中的自定义@ActiveUser - Mike Partridge
@MikePartridge,我好像没找到你说的内容,有链接或更多信息吗? - azerafati
1
我犯了一个错 - 我误解了Spring Security AuthenticationPrincipalArgumentResolver javadoc。文档中展示了一个例子,使用自定义的 @CurrentUser 注解包装 @AuthenticationPrincipal。自从 3.2 版本以后,我们不需要像链接中提到的实现自定义参数解析器了。这个答案 有更多的细节说明。 - Mike Partridge

6
我会这么做:

我只需要这样做:

request.getRemoteUser();

1
可能会起作用,但不可靠。根据Javadoc的说明:“是否在每个后续请求中发送用户名取决于浏览器和身份验证类型。”-http://download-llnw.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser() - Scott Bale
3
在Spring Security web应用程序中获取远程用户名的一个有效且非常简单的方法是使用标准过滤器链中包含的SecurityContextHolderAwareRequestFilter。该过滤器会包装请求并通过访问SecurityContextHolder来实现此调用。 - Shaun the Sheep

5
是的,通常静态代码是不好的——通常情况下是这样,但在这种情况下,静态代码是您可以编写的最安全的代码。由于安全上下文将Principal与当前运行的线程相关联,因此最安全的代码将尽可能直接地从线程中访问静态内容。将访问隐藏在注入的包装类后面会为攻击者提供更多攻击点。他们不需要访问代码(如果JAR已签名,则很难更改),他们只需要一种在运行时覆盖配置的方法,或者将某些XML滑入类路径即可。即使在签名代码中使用注释注入也可以通过外部XML进行覆盖。这种XML可能会向正在运行的系统注入一个恶意主体。这可能就是为什么Spring在这种情况下做出了如此不像Spring的事情。

4
你可以使用Spring AOP方法。例如,如果你有一些需要知道当前负责人的服务,你可以引入自定义注释@Principal,该注释表示此服务应该依赖于负责人。
public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

在您的建议中,我认为需要扩展MethodBeforeAdvice,检查特定服务是否具有@Principal注释,并注入Principal名称,或将其设置为“ANONYMOUS”。


我需要在服务类中访问Principal,你能在Github上发布一个完整的示例吗?我不懂Spring AOP,因此请求帮助。 - Rakesh Waghela

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