Spring Security를 ​​사용할 때 Bean에서 현재 사용자 이름 (예 : SecurityContext) 정보를 얻는 올바른 방법은 무엇입니까?


288

Spring Security를 ​​사용하는 Spring MVC 웹 앱이 있습니다. 현재 로그인 한 사용자의 사용자 이름을 알고 싶습니다. 아래 주어진 코드 스 니펫을 사용하고 있습니다. 이것이 허용되는 방법입니까?

나는이 컨트롤러 내에서 정적 메소드를 호출하는 것을 좋아하지 않는다. 대신 현재 SecurityContext 또는 현재 인증을 주입하도록 앱을 구성하는 방법이 있습니까?

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

수퍼 클래스로 컨트롤러 (보안 컨트롤러)가 SecurityContext에서 사용자를 가져 와서 해당 클래스 내부의 인스턴스 변수로 설정하지 않는 이유는 무엇입니까? 이런 식으로 보안 컨트롤러를 확장하면 전체 클래스가 현재 컨텍스트의 사용자 프린시 펄에 액세스 할 수 있습니다.
Dehan de Croos

답변:


259

Spring 3을 사용하는 경우 가장 쉬운 방법은 다음과 같습니다.

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

     final String currentUser = principal.getName();

 }

69

이 질문에 대한 답을 얻은 후 봄 세계에서 많은 변화가있었습니다. Spring은 컨트롤러에서 현재 사용자를 얻는 것을 단순화했습니다. 다른 빈의 경우 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 문제열었습니다 .

마지막 한 가지-나는 이전에 내가 가진 대답을 크게 바 꾸었습니다. 궁금한 점이 있다면 역사를 확인하십시오.하지만 동료가 지적했듯이 이전 답변은 멀티 스레드 환경에서 작동하지 않습니다. 기본적으로 SecurityContextHolderStrategy사용되는 SecurityContextHolder기본은의 인스턴스이며 ThreadLocalSecurityContextHolderStrategy를에 저장 SecurityContext합니다 ThreadLocal. 따라서 SecurityContext초기화 시간에 Bean에 직접 직접 삽입하는 것이 좋은 아이디어는 아닙니다 ThreadLocal. 멀티 스레드 환경에서 매번 검색해야 할 수 있으므로 올바른 것을 검색해야합니다.


1
나는 당신의 솔루션을 좋아합니다-Spring에서 공장 방법 지원을 영리하게 사용합니다. 컨트롤러 개체가 웹 요청 범위에 속하기 때문에 이것은 당신을 위해 작동합니다. 컨트롤러 Bean의 범위를 잘못된 방식으로 변경 한 경우 중단됩니다.
Paul Morie

2
앞의 두 의견은 방금 교체 한 오래되고 잘못된 답변을 말합니다.
Scott Bale

12
이것이 현재 Spring 릴리스에서 여전히 권장되는 솔루션입니까? 사용자 이름 만 검색하기 위해 너무 많은 코드가 필요하다고 믿을 수 없습니다.
Ta Sas

6
Spring Security 3.0.x를 사용하는 경우 jira.springsource.org/browse/SEC-1188을 기록한 JIRA 문제에 대한 제안을 구현하여 이제 표준을 통해 Bean에 SecurityContextHolderStrategy 인스턴스 (SecurityContextHolder)를 직접 주입 할 수 있습니다. 스프링 구성.
Scott Bale

4
tsunade21 답변을 참조하십시오. Spring 3에서는 이제 컨트롤러에서 메소드 인자로 java.security.Principal을 사용할 수 있습니다
Patrick

22

현재 사용자의 악취에 대해 SecurityContext를 쿼리해야한다는 점에 동의합니다.이 문제를 처리하는 매우 봄의 방법이 아닙니다.

이 문제를 해결하기 위해 정적 "헬퍼"클래스를 작성했습니다. 전역적이고 정적 인 방법이라는 것은 더럽지 만 보안과 관련된 것을 변경하면 적어도 한 곳에서만 세부 정보를 변경해야한다고 생각합니다.

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

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"태그에 추가하십시오.


이것은 아마도 봄 보안 승인 방법 인 것 같습니다!
Nick Spacek

3
이 메소드가 작동하려면 use.expressions = "true"를 security.xml 구성의 http 태그에 추가해야합니다.
SBerg413

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

여기, CustomUsercustom에 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";
    }
}

나는 당신의 해결책을 좋아합니다. 스프링 전문가에게 : 안전하고 좋은 솔루션입니까?
marioosh

좋은 해결책이 아닙니다. 당신은 얻을 것이다 null사용자가 익명으로 인증 된 경우 ( http> anonymous봄 보안 XML 요소). SecurityContextHolder또는 SecurityContextHolderStrategy올바른 방법입니다.
Nowaker

1
null이 아닌지 확인하기 위해 request.getUserPrincipal ()! = null입니다.
digz6666

필터에 null이 있음
Alex78191 1

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 : 팬시 원 : 자세한 내용은 이것을 확인하십시오

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

1
3.2 이후, spring-security-web은 링크 @CurrentUser의 사용자 정의와 같이 작동 @ActiveUser합니다.
Mike Partridge

@ MikePartridge, 나는 당신이 말하는 것을 찾지 못하는 것 같습니다. 또는 더 많은 정보 ??
azerafati 2016 년

1
내 실수 -Spring Security AuthenticationPrincipalArgumentResolver javadoc을 잘못 이해했습니다 . @AuthenticationPrincipal사용자 정의 @CurrentUser주석으로 줄 바꿈하는 예제가 표시 됩니다. 3.2 이후로 우리는 링크 된 답변에서와 같이 사용자 정의 인수 해결자를 구현할 필요가 없습니다. 이 다른 대답 은 더 자세합니다.
Mike Partridge

5

예, 정적은 일반적으로 나쁘지만 일반적으로 정적은 작성할 수있는 가장 안전한 코드입니다. 보안 컨텍스트는 Principal을 현재 실행중인 스레드와 연결하므로 가장 안전한 코드는 가능한 한 스레드에서 직접 정적에 액세스합니다. 주입 된 랩퍼 클래스 뒤에 액세스를 숨기면 공격자에게 더 많은 공격 포인트가 제공됩니다. 코드에 액세스 할 필요가 없으며 (jar이 서명 된 경우 변경하기가 어려울 수 있음) 런타임에 수행하거나 일부 XML을 클래스 경로에 넣을 수있는 구성을 재정의하는 방법이 필요합니다. 서명 된 코드에서 주석 삽입을 사용하더라도 외부 XML로 재정의 할 수 있습니다. 이러한 XML은 실행중인 시스템에 불량 사용자를 주입 할 수 있습니다.


5

나는 이것을 할 것입니다 :

request.getRemoteUser();

1
작동하지만 확실하지는 않습니다. javadoc에서 : "각 후속 요청과 함께 사용자 이름이 전송되는지 여부는 브라우저 및 인증 유형에 따라 다릅니다." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/...
스콧 베일

3
이것은 실제로 Spring Security 웹 애플리케이션에서 원격 사용자 이름을 얻는 유효하고 매우 간단한 방법입니다. 표준 필터 체인에는 SecurityContextHolderAwareRequestFilter요청을 래핑하고에 액세스하여이 호출을 구현하는가 포함됩니다 SecurityContextHolder.
양 Shaun the

4

내가 쓴 마지막 Spring MVC 앱의 경우 SecurityContext 홀더를 주입하지 않았지만 이것과 관련된 두 가지 유틸리티 메소드가있는 기본 컨트롤러가 있습니다 ... isAuthenticated () & getUsername (). 내부적으로 그들은 당신이 설명한 정적 메소드 호출을 수행합니다.

나중에 리팩토링해야 할 경우 적어도 한 번에 있습니다.


3

Spring AOP aproach를 사용할 수 있습니다. 예를 들어 서비스가있는 경우 현재 주체를 알아야합니다. 이 서비스가 주체에 의존해야 함을 나타내는 사용자 정의 주석, 즉 @Principal을 도입 할 수 있습니다.

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

그런 다음 MethodBeforeAdvice를 확장해야한다고 생각하는 특정 서비스에 @Principal 주석이 있는지 확인하고 Principal 이름을 삽입하거나 대신 'ANONYMOUS'로 설정하십시오.


서비스 클래스 내부의 Principal에 액세스해야합니다 .github에 완전한 예제를 게시 할 수 있습니까? 나는 봄 AOP를 모른다. 따라서 요청.
Rakesh Waghela

2

유일한 문제는 Spring Security로 인증 한 후에도 사용자 / 주체 빈이 컨테이너에 존재하지 않기 때문에 의존성 주입이 어렵다는 것입니다. Spring Security를 ​​사용하기 전에 현재 프린시 펄이있는 세션 범위 Bean을 작성하여 "AuthService"에 삽입 한 다음 해당 서비스를 애플리케이션의 다른 서비스 대부분에 주입합니다. 따라서 해당 서비스는 단순히 authService.getCurrentUser ()를 호출하여 객체를 가져옵니다. 코드에서 세션의 동일한 Principal에 대한 참조를 얻을 수있는 위치가 있으면 세션 범위 Bean의 특성으로 간단히 설정할 수 있습니다.


1

이 시도

인증 인증 = SecurityContextHolder.getContext (). getAuthentication ();
문자열 userName = authentication.getName ();


3
SecurityContextHolder.getContext () 정적 메소드를 호출하는 것은 원래 질문에서 내가 불평했던 것과 정확히 같습니다. 당신은 아무것도 대답하지 않았습니다.
스콧 베일

2
그러나 문서에서 권장 하는 내용은 다음과 같습니다. static.springsource.org/spring-security/site/docs/3.0.x/… 그렇다면이를 피함으로써 무엇을 달성하고 있습니까? 간단한 문제에 대한 복잡한 해결책을 찾고 있습니다. 기껏해야 같은 행동을하게됩니다. 최악의 경우, 버그 나 보안 취약점이 발생합니다.
밥 컨

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

(UsernamePasswordAuthenticationToken의 authToken 인스턴스)는 if (authToken! = null)과 기능이 같습니다. 후자는 조금 더 깨끗하지만 차이가 없습니다.
Mark

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

프리 마커 페이지에서 사용자 세부 정보를 지원하는 방법을 공유하고 싶습니다. 모든 것이 매우 간단하고 완벽하게 작동합니다!

인증 요청을 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 ()를 사용하는 것은 피하고 싶었던 것과 처음 에이 질문을 한 이유입니다.
Scott Bale
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.