首页 文章

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

提问于
浏览
267

我有一个使用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 回答

  • 8

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

    @RequestMapping(method = RequestMethod.GET)   
     public ModelAndView showResults(final HttpServletRequest request, Principal principal) {
    
         final String currentUser = principal.getName();
    
     }
    
  • 0

    自从回答这个问题后,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就是这样 .

    最后一件事 - 我很好奇但是,正如一位同事向我指出的那样,我以前的答案在多线程环境中不起作用 . SecurityContextHolder 使用的基础 SecurityContextHolderStrategy 默认情况下是 ThreadLocalSecurityContextHolderStrategy 的实例,它将 SecurityContext 存储在 ThreadLocal 中 . 因此,在初始化时将 SecurityContext 直接注入bean不一定是个好主意 - 每次都需要在多线程环境中从 ThreadLocal 检索它,以便检索正确的 .

  • -1

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

    要使它只显示在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”标记,以使其工作 .

  • 5

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

  • 3

    我通过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";
        }
    }
    
  • 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) {
      ...
    }
    
  • 21

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

  • 1

    我会这样做:

    request.getRemoteUser();
    
  • 13

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

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

  • 242

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

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

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

  • 2

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

  • 21

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

    我在 @Controller 类中使用 @AuthenticationPrincipal 注释以及在 @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
    }
    

    真的很容易

  • 55

    试试这个

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

  • 5

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

  • 1

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

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

    这是我的ftl代码:

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

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

相关问题