使用Spring Security时,在bean中获取当前用户名(即SecurityContext)信息的正确方法是什么?


288

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

我不喜欢在此控制器内调用静态方法-这违反了Spring IMHO的全部目的。有没有一种方法可以配置应用程序以注入当前的SecurityContext或当前的Authentication?

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

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

Answers:


258

如果您使用的是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一个getter来获取基础SecurityContextHolderStrategy实例(这是一个接口),则可以注入它。实际上,我什至为此揭开了一个Jira问题

最后一件事-我刚刚改变了我以前在这里得到的答案。如果您好奇,请检查历史记录,但是,正如一位同事向我指出的那样,我以前的回答在多线程环境中不起作用。底层SecurityContextHolderStrategy使用SecurityContextHolder,默认情况下,的一个实例ThreadLocalSecurityContextHolderStrategy,其存储SecurityContextS IN一个ThreadLocal。因此,不一定要SecurityContext在初始化时将它直接注入Bean中-可能需要从Bean中检索它。ThreadLocal在多线程环境中,每次都,以便检索正确的一个。


1
我喜欢您的解决方案-在Spring中巧妙地使用了工厂方法支持。就是说,这对您有用,因为控制器对象的作用域是Web请求。如果以错误的方式更改了控制器bean的作用域,则可能会中断。
保罗·莫里

2
前面的两个评论指向的是我刚刚替换的旧的,错误的答案。
Scott Bale

12
在当前的Spring版本中,这仍然是推荐的解决方案吗?我不敢相信它只需要检索用户名就需要太多代码。
塔萨斯

6
如果您使用的是Spring Security 3.0.x,那么他们会在我登录jira.springsource.org/browse/SEC-1188的JIRA问题中实现我的建议,因此您现在可以通过标准将SecurityContextHolderStrategy实例(来自SecurityContextHolder)直接注入到bean中弹簧配置。
Scott Bale 2010年

4
请参阅tsunade21的答案。Spring 3现在允许您将java.security.Principal用作控制器中的方法参数
Patrick

22

我同意必须查询SecurityContext来获取当前用户的臭味,这似乎是一种非Spring的方法来处理此问题。

我写了一个静态的“ helper”类来解决这个问题。它很脏,因为它是一种全局和静态方法,但是我想出了这种方式,如果我们更改与安全性相关的任何内容,至少我只需要在一个地方更改细节:

/**
* 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中。此代码不保持任何状态。
马特b

22

要使其仅显示在您的JSP页面中,可以使用Spring Security Tag Lib:

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

要使用任何标记,必须在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”

到security.xml配置中的“ http”标签,即可正常工作。


看来这可能是Spring Security批准的方式!
Nick Spacek 2012年

3
为了使此方法起作用,您需要在security.xml配置中的http标记中添加use-expressions =“ true”。
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由custom返回的实现的自定义对象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 2011年

不是一个好的解决方案。null如果用户被匿名认证(Spring Security XML中的http> anonymous元素),您将获得。SecurityContextHolder还是SecurityContextHolderStrategy正确的方法。
Nowaker

1
因此,我检查了是否为null request.getUserPrincipal()!= null。
digz6666

在Filter中为null
Alex78191 '18

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
从3.2开始,spring-security-web自带,@CurrentUser其功能类似于@ActiveUser您链接中的自定义。
Mike Partridge14年

@MikePartridge,我似乎没找到你说的话,任何链接?或更多信息?
azerafati 2015年

1
我的错误-我误解了Spring Security AuthenticationPrincipalArgumentResolver javadoc。此处显示的示例@AuthenticationPrincipal带有自定义@CurrentUser注释。从3.2开始,我们不需要像链接答案中那样实现自定义参数解析器。这个其他答案有更多细节。
迈克·帕特里奇

5

是的,静态变量通常是不好的-通常,但是在这种情况下,静态变量是您可以编写的最安全的代码。由于安全上下文将Principal与当前正在运行的线程相关联,因此最安全的代码将尽可能直接从线程访问静态变量。将访问隐藏在注入的包装器类之后,可以为攻击者提供更多要攻击的点。他们不需要访问代码(如果对jar进行签名,将很难更改代码),他们只需要一种覆盖配置的方法即可,这可以在运行时完成,也可以将一些XML放到类路径中。即使在签名的代码中使用注解注入,也可以被外部XML覆盖。这样的XML可以向恶意运行主体注入正在运行的系统。


5

我会这样做:

request.getRemoteUser();

1
这可能有效,但不可靠。来自javadoc:“用户名是否随每个后续请求一起发送,取决于浏览器和身份验证类型。” - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/...
斯科特罢了

3
实际上,这是在Spring Security Web应用程序中获取远程用户名的有效且非常简单的方法。标准过滤器链包括一个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批注并注入Principal名称,或者将其设置为“ ANONYMOUS”。


我需要在服务类内部访问Principal,您可以在github上发布完整的示例吗?我不知道春季AOP,因此不知道该要求。
Rakesh Waghela

2

唯一的问题是,即使在通过Spring Security进行身份验证之后,容器中也不存在用户/主体Bean,因此注入依赖项将很困难。在使用Spring Security之前,我们将创建一个具有当前Principal的会话范围的Bean,将其注入“ AuthService”,然后将该Service注入应用程序中的大多数其他服务。因此,这些服务只需调用authService.getCurrentUser()即可获取对象。如果您在代码中有一个位置可以在会话中引用相同的Principal,则只需将其设置为会话作用域bean的属性即可。


1

尝试这个

身份验证身份验证= SecurityContextHolder.getContext()。getAuthentication();
字符串userName = authentication.getName();


3
调用SecurityContextHolder.getContext()静态方法正是我在原始问题中抱怨的问题。你什么都没回答。
Scott Bale

2
但是,它正是文档所建议的: static.springsource.org/spring-security/site/docs/3.0.x / ... 那么,避免使用它会做什么呢?您正在寻找针对简单问题的复杂解决方案。充其量-您会得到相同的行为。最糟糕的是,您会遇到错误或安全漏洞。
Bob Kerns 2012年

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实例检查?
Scott Bale

(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

我想在freemarker页面上分享支持用户详细信息的方式。一切都非常简单并且完美地工作!

您只需要在default-target-url(表单登录后的页面)上放置Authentication rerequest,这是我对该页面的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()正是我要避免的原因,也是我首先问这个问题的原因。
Scott Bale
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.