簡體   English   中英

使用 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 的 Spring MVC Web 應用程序。 我想知道當前登錄用戶的用戶名。 我正在使用下面給出的代碼片段。 這是公認的方式嗎?

我不喜歡在這個控制器中調用靜態方法 - 這違背了 Spring 的全部目的,恕我直言。 有沒有辦法將應用程序配置為注入當前的 SecurityContext 或當前的身份驗證?

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

如果您使用的是Spring 3 ,最簡單的方法是:

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

     final String currentUser = principal.getName();

 }

自從回答這個問題以來,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 問題

最后一件事 - 我剛剛大大改變了我之前在這里的答案。 如果您好奇,請查看歷史記錄,但正如一位同事向我指出的那樣,我之前的答案在多線程環境中不起作用。 底層SecurityContextHolderStrategy使用SecurityContextHolder ,默認情況下,的一個實例ThreadLocalSecurityContextHolderStrategy ,其存儲SecurityContext S IN一個ThreadLocal 因此,在初始化時將SecurityContext直接注入 bean 不一定是一個好主意 - 在多線程環境中,每次可能需要從ThreadLocal檢索它,因此檢索到正確的。

我同意必須查詢當前用戶的 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;
}

為了讓它只顯示在你的 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”標記以使其工作。

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

我通過 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 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:花式一:查看此了解更多詳細信息

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

我只會這樣做:

request.getRemoteUser();

是的,靜態通常是不好的 - 通常,但在這種情況下,靜態是您可以編寫的最安全的代碼。 由於安全上下文將 Principal 與當前運行的線程相關聯,因此最安全的代碼將盡可能直接地從線程訪問靜態。 將訪問隱藏在注入的包裝類后面,為攻擊者提供了更多攻擊點。 他們不需要訪問代碼(如果 jar 被簽名,他們將很難更改代碼),他們只需要一種覆蓋配置的方法,這可以在運行時完成或將一些 XML 滑到類路徑上。 即使在簽名代碼中使用注解注入也可以被外部 XML 覆蓋。 這樣的 XML 可能會向正在運行的系統注入一個流氓主體。 這可能就是為什么在這種情況下 Spring 會做一些與 Spring 不同的事情。

對於我寫的最后一個 Spring MVC 應用程序,我沒有注入 SecurityContext 持有者,但我確實有一個基本控制器,我有兩個與此相關的實用程序方法...... isAuthenticated() & getUsername()。 在內部,他們執行您描述的靜態方法調用。

至少,如果您以后需要重構,它只會出現一次。

您可以使用 Spring AOP 方法。 例如,如果您有一些服務,則需要知道當前的委托人。 您可以引入自定義注釋,即 @Principal ,這表明該服務應該是主體相關的。

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

然后在我認為需要擴展 MethodBeforeAdvice 的建議中,檢查特定服務是否具有 @Principal 注釋並注入主體名稱,或者將其設置為“ANONYMOUS”。

唯一的問題是,即使在使用 Spring Security 進行身份驗證后,容器中也不存在用戶/主體 bean,因此依賴注入它會很困難。 在我們使用 Spring Security 之前,我們將創建一個具有當前 Principal 的 session-scoped bean,將它注入到“AuthService”中,然后將該 Service 注入到應用程序中的大多數其他服務中。 因此,這些服務將簡單地調用 authService.getCurrentUser() 來獲取對象。 如果您的代碼中有一個地方可以在會話中獲得對同一 Principal 的引用,則只需將其設置為會話范圍 bean 上的屬性即可。

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

    }

我現在用的是@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
}

真的很容易。

Principal定義為控制器方法中的依賴項,spring 將在調用時將當前經過身份驗證的用戶注入到您的方法中。

嘗試這個

認證身份驗證 = SecurityContextHolder.getContext().getAuthentication();
String userName = authentication.getName();

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

就是這樣,用戶名將在授權后出現在每個頁面上。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM