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

Posted

技术标签:

【中文标题】使用 Spring Security 时,在 bean 中获取当前用户名(即 SecurityContext)信息的正确方法是啥?【英文标题】:When using Spring Security, what is the proper way to obtain current username (i.e. SecurityContext) information in a bean?使用 Spring Security 时,在 bean 中获取当前用户名(即 SecurityContext)信息的正确方法是什么? 【发布时间】:2019-04-03 20:38:27 【问题描述】:

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

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

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

【问题讨论】:

为什么你没有一个控制器(安全控制器)作为超类从 SecurityContext 获取用户并将其设置为该类中的实例变量?这样,当您扩展安全控制器时,您的整个类都可以访问当前上下文的用户主体。 【参考方案1】:

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

如果您以后需要重构,至少只有一次。

【讨论】:

【参考方案2】:

您可以使用 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,因此请求。【参考方案3】:

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

【讨论】:

【参考方案4】:

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

【讨论】:

它与 SecurityContextHolder.getContext() 一样长,后者是线程安全的,因为它将安全细节保存在 threadLocal 中。此代码不维护任何状态。【参考方案5】:

自从回答了这个问题后,Spring 世界发生了很多变化。 Spring 简化了在控制器中获取当前用户的过程。对于其他bean,Spring采纳了作者的建议,简化了'SecurityContextHolder'的注入。更多细节在 cmets 中。


这是我最终采用的解决方案。我不想在我的控制器中使用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 达到了这个效果。

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

【讨论】:

我喜欢你的解决方案——它巧妙地使用了 Spring 中的工厂方法支持。也就是说,这对您有用,因为控制器对象的范围仅限于 Web 请求。如果您以错误的方式更改了控制器 bean 的范围,则会中断。 前两个 cmets 指的是我刚刚替换的旧的错误答案。 这仍然是当前 Spring 版本的推荐解决方案吗?我不敢相信它需要这么多代码来检索用户名。 如果您使用的是 Spring Security 3.0.x,他们在我记录的jira.springsource.org/browse/SEC-1188 JIRA 问题中实现了我的建议,因此您现在可以通过标准将 SecurityContextHolderStrategy 实例(来自 SecurityContextHolder)直接注入您的 bean弹簧配置。 请看 tsunade21 的回答。 Spring 3 现在允许您在控制器中使用 java.security.Principal 作为方法参数【参考方案6】:

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

【讨论】:

【参考方案7】:

我会这样做:

request.getRemoteUser();

【讨论】:

这可能有效,但不可靠。来自 javadoc:“是否随每个后续请求发送用户名取决于浏览器和身份验证类型。” -download-llnw.oracle.com/javaee/6/api/javax/servlet/http/… 这实际上是一种在 Spring Security Web 应用程序中获取远程用户名的有效且非常简单的方法。标准过滤器链包含一个SecurityContextHolderAwareRequestFilter,它封装了请求并通过访问SecurityContextHolder 来实现此调用。【参考方案8】:

我通过 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 专家:这是安全、好的解决方案吗? 不是一个好的解决方案。如果用户通过匿名身份验证(http > anonymous Spring Security XML 中的元素),您将获得 nullSecurityContextHolderSecurityContextHolderStrategy 是正确的方法。 所以我检查了如果不是 null request.getUserPrincipal() != null. 在过滤器中为空【参考方案9】:

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

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

     final String currentUser = principal.getName();

 

【讨论】:

【参考方案10】:

试试这个

认证认证= SecurityContextHolder.getContext().getAuthentication(); String userName = authentication.getName();

【讨论】:

调用 SecurityContextHolder.getContext() 静态方法正是我在原始问题中所抱怨的。你还没有回答任何问题。 然而,这正是文档所建议的:static.springsource.org/spring-security/site/docs/3.0.x/… 那么您通过避免它来完成什么?您正在寻找一个简单问题的复杂解决方案。充其量 - 你得到相同的行为。在最坏的情况下,您会遇到错误或安全漏洞。 @BobKerns 对于测试,能够注入身份验证比将其放在本地线程上更简洁。【参考方案11】:

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

    

【讨论】:

当参数已经是 UsernamePasswordAuthenticationToken 类型时,为什么还要进行 instanceof UsernamePasswordAuthenticationToken 检查? (authToken instanceof UsernamePasswordAuthenticationToken) 在功能上等同于 if (authToken !=null)。后者可能更干净一点,但除此之外没有区别。【参考方案12】:

要使其仅显示在您的 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 在 cmets 中提到的,您需要添加

use-expressions="true"

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

【讨论】:

这似乎是 Spring Security 认可的方式! 要使此方法生效,您需要将 use-expressions="true" 添加到 security.xml 配置中的 http 标记。 感谢@SBerg413,我将编辑我的答案并添加您的重要说明!【参考方案13】:

我喜欢在 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> 

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

【讨论】:

感谢您尝试回答,但是使用静态方法 SecurityContextHolder.getContext() 正是我想要避免的,也是我首先提出这个问题的原因。【参考方案14】:

在 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) 
  ...

【讨论】:

自 3.2 起,spring-security-web 带有 @CurrentUser,其工作方式类似于您链接中的自定义 @ActiveUser @MikePartridge,我好像没找到你说的,有链接吗??或更多信息?? 我的错误 - 我误解了 Spring Security AuthenticationPrincipalArgumentResolver javadoc。此处显示了一个示例,该示例使用自定义 @CurrentUser 注释包装 @AuthenticationPrincipal。从 3.2 开始,我们不需要像链接答案中那样实现自定义参数解析器。 This other answer 有更多细节。【参考方案15】:

我在@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

真的很简单。

【讨论】:

【参考方案16】:

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

【讨论】:

【参考方案17】:

如果您使用的是 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 章节中找到。

【讨论】:

以上是关于使用 Spring Security 时,在 bean 中获取当前用户名(即 SecurityContext)信息的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

User must be authenticated with Spring Security before authorization can be completed.解决方法

使用spring security的spring boot:创建名为“securityFilterChainRegistration”的bean时出错

spring-security中的 No AuthenticationEntryPoint could be established异常处理.

How to configure Spring Security to allow Swagger URL to be accessed without authentication

Spring Security 4. 指定访问决策管理器时出现异常

Spring Security 4. 指定访问决策管理器时出现异常