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

我不喜欢在这个控制器中调用静态方法——恕我直言,这违背了Spring的全部目的。有没有一种方法来配置应用程序有当前的SecurityContext,或当前的认证,注入代替?

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

当前回答

您可以使用Spring AOP方法。 例如,如果你有一些服务,它需要知道当前的本金。您可以引入自定义注释,例如@Principal,它表明该服务应该是依赖于主体的。

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

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

其他回答

我使用@Controller类中的@AuthenticationPrincipal注释以及@ controlleradvisor注释。例:

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

真的很容易。

在回答了这个问题之后,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实例(这是一个接口),你就可以注入它。事实上,我甚至为此开了一期Jira杂志。

One last thing - I've just substantially changed the answer I had here before. Check the history if you're curious but, as a coworker pointed out to me, my previous answer would not work in a multi-threaded environment. The underlying SecurityContextHolderStrategy used by SecurityContextHolder is, by default, an instance of ThreadLocalSecurityContextHolderStrategy, which stores SecurityContexts in a ThreadLocal. Therefore, it is not necessarily a good idea to inject the SecurityContext directly into a bean at initialization time - it may need to be retrieved from the ThreadLocal each time, in a multi-threaded environment, so the correct one is retrieved.

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

至少在以后需要重构时,它只在一个地方。

为了让它只显示在JSP页面中,你可以使用Spring Security标签库:

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 ver >= 3.2,您可以使用@AuthenticationPrincipal注释:

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

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

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