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

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

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

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

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

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

你可以使用Spring AOP方法。 例如,如果你有一些服务,它需要知道当前的本金。您可以引入自定义注释,例如@Principal,这表明该服务应该是依赖于主体的
public class SomeService {
private String principal;
@Principal
public setPrincipal(String principal){
this.principal=principal;
}
}

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

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

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

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

最后一件事,我基本上改变了之前的答案。如果你好奇,可以查看历史记录,但是正如一位同事向我指出的那样,我之前的答案在多线程环境中不起作用。默认情况下,SecurityContextHolder使用的底层SecurityContextHolderStrategyThreadLocalSecurityContextHolderStrategy的一个实例,它将__abc3存储在ThreadLocal中。因此,在初始化时直接将SecurityContext注入到bean中并不一定是个好主意——在多线程环境中,每次都可能需要从ThreadLocal中检索它,因此会检索正确的那个。

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

我会这样做:

request.getRemoteUser();

I get authenticated user by < / p > 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";
}
}

如果你正在使用春天3,最简单的方法是:

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


final String currentUser = principal.getName();


}

试试这个

Authentication Authentication = < br > .getAuthentication SecurityContextHolder.getContext () ();

. getname ()

如果您正在使用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();
}
...


}

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

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html < a href = " 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”标签以使其工作。

我喜欢在freemarker页面上分享我支持用户详细信息的方法。 一切都很简单,工作完美!< / p > 你只需要在default-target-url上放置身份验证重新请求(表单登录后的页面) 这是我的控制器方法的页面:

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

这是我的超光速代码:

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

就是这样,用户名将出现在授权后的每一页。

在Spring 3+中,您有以下选项。

方案一:

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByPrincipal(Principal principal) {
return principal.getName();
}

选择二:

@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) {
...
}

我在@Controller类以及@ControllerAdvicer注释类中使用了@AuthenticationPrincipal注释。例:

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

真的很容易。

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

如果你正在使用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章节中找到。