Spring Securityを使用する場合、Beanで現在のユーザー名(つまりSecurityContext)情報を取得する適切な方法は何ですか?


288

Spring Securityを使用するSpring MVC Webアプリがあります。現在ログインしているユーザーのユーザー名を知りたい。以下のコードスニペットを使用しています。これは受け入れられる方法ですか?

私はこのコントローラー内で静的メソッドを呼び出すのが好きではありません-これは、私見のSpringの目的全体を無効にします。代わりに、現在のSecurityContextまたは現在の認証が挿入されるようにアプリを構成する方法はありますか?

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

スーパークラスがユーザーをSecurityContextから取得し、そのクラス内のインスタンス変数として設定するため、コントローラー(セキュアコントローラー)がないのはなぜですか?このようにしてセキュアコントローラーを拡張すると、クラス全体が現在のコンテキストのユーザープリンシパルにアクセスできるようになります。
Dehan de Croos 2017

回答:


259

Spring 3を使用している場合、最も簡単な方法は次のとおりです。

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

     final String currentUser = principal.getName();

 }

69

この質問が答えられて以来、春の世界では多くの変化がありました。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が同様のものを注入する方法を提供していないことは、少しばかげているように思えます。SecurityContextHolderacegiから受け継がれたのは分かりますが、それでも。問題は、それらが非常に近いことです。SecurityContextHolder基盤となるSecurityContextHolderStrategyインスタンス(インターフェース)を取得するためのゲッターさえあれば、それを注入できます。実際、私はそのためにJiraの問題さえも開きました

最後にもう1つ、これまでの答えを大幅に変更しました。興味があれば履歴を確認してください。ただし、同僚が私に指摘したように、私の以前の回答はマルチスレッド環境では機能しません。SecurityContextHolderStrategyによって使用される基になるものSecurityContextHolderは、デフォルトではのインスタンスでありThreadLocalSecurityContextHolderStrategySecurityContexts をに格納しThreadLocalます。したがって、SecurityContext初期化時にを直接Bean に注入することは必ずしも良い考えではありません。ThreadLocalマルチスレッド環境では毎回から取得する必要がある場合があるため、正しいものが取得されます。


1
私はあなたの解決策が好きです-それはSpringのファクトリメソッドサポートの賢い使い方です。とは言っても、コントローラーオブジェクトのスコープはWebリクエストに限定されているため、これはうまくいきます。コントローラBeanのスコープを間違った方法で変更した場合、これは壊れます。
ポール・森江

2
前の2つのコメントは、私が置き換えたばかりの古い不正確な回答を参照しています。
スコットベール

12
これは、現在のSpringリリースでまだ推奨されるソリューションですか?ユーザー名だけを取得するためだけに多くのコードが必要だとは信じられません。
Ta Sas 2010

6
Spring Security 3.0.xを使用している場合、私はjira.springsource.org/browse/SEC-1188に記録したJIRAの問題に私の提案を実装したので、(SecurityContextHolderからの)SecurityContextHolderStrategyインスタンスを標準を介して直接Beanに注入できますスプリング構成。
スコットベール

4
綱手21の回答をご覧ください。Spring 3では、コントローラーのメソッド引数としてjava.security.Principalを使用できるようになりました
Patrick

22

現在のユーザーのSecurityContextをクエリする必要があるので、この問題を処理するのは非常に非Springの方法のようです。

この問題に対処するために、静的な「ヘルパー」クラスを作成しました。それはグローバルで静的な方法であるという点でダーティですが、セキュリティに関連する何かを変更する場合、少なくとも1か所で詳細を変更するだけでよいので、このように考えました。

/**
* 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;
}

22
SecurityContextHolder.getContext()と同じ長さであり、後者はセキュリティの詳細をthreadLocalに保持するため、スレッドセーフです。このコードは状態を維持しません。
matt b '29

22

それをJSPページに表示するだけの場合は、Spring Security Tag Libを使用できます。

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

タグを使用するには、JSPでセキュリティtaglibを宣言する必要があります。

<%@ 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承認の方法のようです!
Nick Spacek 2012

3
このメソッドを機能させるには、security.xml構成のhttpタグにuse-expressions = "true"を追加する必要があります。
SBerg413 2013年

@ SBerg413に感謝します。私の回答を編集して、重要な説明を追加します。
Brad Parks

14

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の章をご覧ください。


13

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のスペシャリストへ:それは安全で優れたソリューションですか?
marioosh

良い解決策ではありません。nullユーザーが匿名で認証されているかどうかを確認します(http> anonymousSpring Security XMLの要素)。SecurityContextHolderまたはSecurityContextHolderStrategy適切な方法です。
Nowaker、2011

1
nullではないかどうかを確認したので、request.getUserPrincipal()!= nullです。
digz6666 2011

フィルターでnullです
Alex78191 2018年

9

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:ファンシー1:詳細はこちらをご覧ください

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

1
3.2以降、リンクから@CurrentUserのカスタムのように機能するspring-security-webが付属してい@ActiveUserます。
マイクパートリッジ2014

@MikePartridge、私はあなたが言っていること、リンクを見つけられないようです?? 以上の情報?
azerafati 2015年

1
私の間違い-Spring Security AuthenticationPrincipalArgumentResolver javadoc を誤解しました。@AuthenticationPrincipalカスタム@CurrentUserアノテーションで折り返す例を示します。3.2以降、リンクされた回答のようにカスタム引数リゾルバーを実装する必要はありません。この他の答えはより詳細です。
マイクパートリッジ

5

はい、静的は一般的に悪いです-一般に、この場合、静的はあなたが書くことができる最も安全なコードです。セキュリティコンテキストはPrincipalを現在実行中のスレッドに関連付けるため、最も安全なコードはスレッドからstaticに可能な限り直接アクセスします。挿入されたラッパークラスの背後でアクセスを非表示にすると、攻撃者は攻撃するポイントが増えます。コードにアクセスする必要はありません(jarが署名されている場合は変更が困難です)。構成をオーバーライドする方法が必要なだけです。これは、実行時に行うか、XMLをクラスパスに挿入することで実行できます。署名されたコードでアノテーションインジェクションを使用しても、外部XMLでオーバーライドできます。このようなXMLは、実行中のシステムに不正なプリンシパルを挿入する可能性があります。


5

私はこれを行うでしょう:

request.getRemoteUser();

1
それはうまくいくかもしれませんが、確実ではありません。javadocから:「後続の各要求でユーザー名が送信されるかどうかは、ブラウザーと認証のタイプによって異なります。」- download-llnw.oracle.com/javaee/6/api/javax/servlet/http/...
スコット・ベール

3
これは実際には、Spring Security Webアプリケーションでリモートユーザー名を取得するための有効で非常にシンプルな方法です。標準のフィルターチェーンにはが含まれてSecurityContextHolderAwareRequestFilterおり、リクエストをラップし、にアクセスすることでこの呼び出しを実装しますSecurityContextHolder
Shaun the Sheep 2012年

4

私が書いた最後のSpring MVCアプリでは、SecurityContextホルダーを挿入しませんでしたが、これに関連する2つのユーティリティメソッドがあるベースコントローラーがありました... isAuthenticated()&getUsername()。内部的には、説明した静的メソッド呼び出しを行います。

少なくとも、後でリファクタリングする必要がある場合、それは一度だけです。


3

Spring AOPアプローチを使用できます。たとえば、サービスがある場合、現在のプリンシパルを知る必要があります。このサービスがプリンシパルに依存する必要があることを示すカスタムアノテーション、つまり@Principalを導入できます。

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

次に、MethodBeforeAdviceを拡張する必要があると思うアドバイスで、特定のサービスに@Principalアノテーションが付いていることを確認し、プリンシパル名を挿入するか、代わりに「ANONYMOUS」に設定します。


サービスクラス内でプリンシパルにアクセスする必要があります。完全な例をgithubに投稿できますか?私は春のAOPを知りません。
Rakesh Waghela 2013

2

唯一の問題は、Spring Securityで認証した後でも、ユーザー/プリンシパルBeanがコンテナーに存在しないため、依存関係を挿入することが難しいことです。Spring Securityを使用する前に、現在のプリンシパルを持つセッションスコープのBeanを作成し、それを「AuthService」に挿入してから、そのサービスをアプリケーションの他のほとんどのサービスに挿入しました。したがって、これらのサービスは単にauthService.getCurrentUser()を呼び出してオブジェクトを取得します。セッション内で同じプリンシパルへの参照を取得するコード内の場所がある場合、それをセッションスコープのBeanのプロパティとして設定できます。


1

これを試して

認証authentication = SecurityContextHolder.getContext()。getAuthentication();
String userName = authentication.getName();


3
SecurityContextHolder.getContext()静的メソッドの呼び出しは、元の質問で私が不満を言っていたこととまったく同じです。何も答えていません。
スコットベール

2
しかし、それはまさにドキュメントが推奨する ものです: static.springsource.org/spring-security/site/docs/3.0.x/…それで、それを回避することによって何を達成していますか?単純な問題に対する複雑な解決策を探しています。せいぜい、同じ動作が得られます。最悪の場合、バグやセキュリティホールが発生します。
ボブ・カーンズ2012

2
@BobKernsテストでは、認証をスレッドローカルに置くのではなく、注入できるようにした方がきれいです。

1

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();
            }
            ...

    }

1
パラメータがすでにUsernamePasswordAuthenticationTokenタイプであるのに、なぜUsernamePasswordAuthenticationTokenチェックのインスタンスを実行するのですか?
Scott Bale

(authToken instanceof UsernamePasswordAuthenticationToken)は、if(authToken!= null)と機能的に同等です。後者は少しクリーンかもしれませんが、それ以外は違いはありません。
マーク

1

私が使用しています@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
}

本当に簡単です。


0

Principalコントローラメソッドで依存関係として定義すると、Springは呼び出し時に現在の認証済みユーザーをメソッドに挿入します。


-2

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> 

そして、それだけです、ユーザー名は承認後にすべてのページに表示されます。


答えてくれてありがとうございますが、静的メソッドSecurityContextHolder.getContext()の使用はまさに避けたいものであり、最初にこの質問をした理由です。
スコットベール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.