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

时间:2008-10-29 21:50:18

标签: java spring spring-mvc spring-security

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

我不喜欢在这个控制器内调用静态方法 - 这违背了Spring的全部目的,恕我直言。有没有办法配置应用程序以注入当前的SecurityContext或当前的身份验证?

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

17 个答案:

答案 0 :(得分:245)

如果您使用Spring 3,最简单的方法是:

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

     final String currentUser = principal.getName();

 }

答案 1 :(得分:58)

自从这个问题得到解答后,Spring世界发生了很多变化。 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实例(这是一个接口),你可以注入它。事实上,我甚至opened a Jira issue就是这样。

最后一件事 - 我刚刚改变了我之前的答案。如果你很好奇,请查看历史记录但是,正如同事指出的那样,我之前的回答在多线程环境中不起作用。默认情况下,SecurityContextHolderStrategy使用的基础SecurityContextHolderThreadLocalSecurityContextHolderStrategy的实例,它将SecurityContext存储在ThreadLocal中。因此,在初始化时将SecurityContext直接注入bean不一定是个好主意 - 每次都需要在多线程环境中从ThreadLocal检索它,所以检索到正确的。

答案 2 :(得分:21)

要使它只显示在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的评论中所述,您需要添加

  

使用表达式= “真”

到security.xml配置中的“http”标记,以使其正常工作。

答案 3 :(得分:21)

我同意不得不为当前用户查询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;
}

答案 4 :(得分:14)

如果您使用的是Spring Security ver&gt; = 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章节中找到更多信息。

答案 5 :(得分: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";
    }
}

答案 6 :(得分:8)

在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:花哨的一个:Check this out for more details

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

答案 7 :(得分:5)

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

答案 8 :(得分:5)

我会这样做:

request.getRemoteUser();

答案 9 :(得分:4)

对于我写的最后一个Spring MVC应用程序,我没有注入SecurityContext持有者,但我确实有一个基本控制器,我有两个与此相关的实用方法... isAuthenticated()&amp; getUsername()。在内部,他们执行您描述的静态方法调用。

至少那时如果你需要稍后重构它只会在一次。

答案 10 :(得分:3)

你可以使用Spring AOP aproach。 例如,如果您有某些服务,则需要了解当前主体。您可以引入自定义注释,即@Principal,它表示此服务应该是主要依赖的。

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

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

答案 11 :(得分:2)

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

答案 12 :(得分:1)

试试这个

  

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

答案 13 :(得分: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();
            }
            ...

    }

答案 14 :(得分: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
}

真的很容易。

答案 15 :(得分:0)

Principal定义为控制器方法中的依赖项,spring将在调用时在方法中注入当前经过身份验证的用户。

答案 16 :(得分:-1)

我想在freemarker页面上分享支持用户详细信息的方式。 一切都很简单,工作得很好!

您只需在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");
}

这是我的ftl代码:

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

就是这样,用户名将在授权后出现在每个页面上。