Spring Securityを使用する場合、現在のユーザー名を取得する適切な方法は何ですか(つまり、SecurityContext) 情報は Bean に含まれますか?
-
05-07-2019 - |
質問
Spring Securityを使用するSpring MVC Webアプリがあります。現在ログインしているユーザーのユーザー名を知りたいです。以下に示すコード スニペットを使用しています。これは受け入れられた方法ですか?
私は、このコントローラー内で静的メソッドを呼び出すのは好きではありません。これは Spring の目的全体を損なうものです。代わりに現在の SecurityContext または現在の Authentication が挿入されるようにアプリを構成する方法はありますか?
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request...) {
final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
...
}
解決
春3 、最も簡単な方法は次のとおりです。
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(final HttpServletRequest request, Principal principal) {
final String currentUser = principal.getName();
}
他のヒント
この質問に答えられてから、春の世界では多くのことが変わりました。 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
のみが、基になる SecurityContextHolderStrategy
インスタンス(インターフェース)を取得するゲッターを持っている場合、それを注入できます。実際、そのためにJiraの問題を開きました。
最後にもう1つ-以前ここで得た答えを大幅に変更しました。興味がある場合は履歴を確認してください。しかし、同僚が私に指摘したように、私の以前の答えはマルチスレッド環境では機能しません。 SecurityContextHolder
が使用する基になる SecurityContextHolderStrategy
は、デフォルトでは ThreadLocalSecurityContextHolderStrategy
のインスタンスで、 SecurityContext
を ThreadLocal
。したがって、初期化時にBeanに SecurityContext
を直接注入することは必ずしも良い考えではありません-毎回 ThreadLocal
から取得する必要があるかもしれません。 -スレッド化された環境なので、正しい環境が取得されます。
現在のユーザーのSecurityContextを照会する必要があることに同意しますが、この問題を処理するのは非常に春ではないようです。
静的な「ヘルパー」を作成しました;この問題に対処するクラス。グローバルで静的なメソッドであるという点で汚いですが、セキュリティに関連する何かを変更した場合、少なくとも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;
}
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 =&quot; true&quot;
&quot; http&quot;へこれが機能するようにsecurity.xml構成内のタグ。
Spring Security バージョン 3.2 以上を使用している場合は、 @AuthenticationPrincipal
注釈:
@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
String currentUsername = currentUser.getUsername();
// ...
}
ここ、 CustomUser
を実装するカスタム オブジェクトです。 UserDetails
税関によって返されたもの UserDetailsService
.
詳細については、 @認証プリンシパル Spring Security リファレンス ドキュメントの章。
認証されたユーザーを取得する 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:ファンシー1:詳細についてはこちらをご覧ください
public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
...
}
はい、静的は一般に悪いです-一般的に、しかし、この場合、静的はあなたが書くことができる最も安全なコードです。セキュリティコンテキストはプリンシパルを現在実行中のスレッドに関連付けるため、最も安全なコードは、可能な限り直接スレッドから静的にアクセスします。挿入されたラッパークラスの背後にアクセスを隠すと、攻撃者は攻撃するポイントが増えます。コードにアクセスする必要はなく(jarが署名されている場合は変更に苦労します)、構成をオーバーライドする方法が必要です。これは、実行時に行うか、XMLをクラスパスにスリップすることができます。署名されたコードでアノテーションインジェクションを使用しても、外部XMLでオーバーライドできます。このようなXMLは、実行中のシステムに不正なプリンシパルを挿入する可能性があります。これがおそらく、Springがこの場合、Springに似ていない何かをしている理由です。
これを行うだけです:
request.getRemoteUser();
前回書いたSpring MVCアプリでは、SecurityContextホルダーを挿入しませんでしたが、これに関連する2つのユーティリティメソッドがあるベースコントローラーがありました... isAuthenticated()&amp; 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を使用する前に、現在のプリンシパルを持つセッションスコープBeanを作成し、それを&quot; AuthService&quot;に挿入しました。次に、そのサービスをアプリケーション内の他のほとんどのサービスに注入します。したがって、これらのサービスは単にauthService.getCurrentUser()を呼び出してオブジェクトを取得します。セッション内で同じプリンシパルへの参照を取得する場所がコードにある場合、セッションスコープBeanのプロパティとして単純に設定できます。
これを試してください
認証認証= SecurityContextHolder.getContext()。getAuthentication();
文字列userName = authentication.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();
}
...
}
@ControllerAdvicer
注釈付きクラスと同様に、 @Controller
クラスで @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は呼び出し時にメソッドに現在の認証済みユーザーを挿入します。
Freemarkerページでユーザーの詳細をサポートする方法を共有したいと思います。 すべてが非常にシンプルで完璧に機能しています!
default-target-url
(form-loginの後のページ)に認証再要求を配置するだけです。
これはそのページの私の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>
これで、承認後、すべてのページにユーザー名が表示されます。