Spring Security / SpringMVC에서 인증 된 사용자를 수동으로 설정하는 방법


107

새 사용자가 '새 계정'양식을 제출 한 후 다음 페이지에서 로그인 할 필요가 없도록 해당 사용자를 수동으로 로그인하고 싶습니다.

스프링 보안 인터셉터를 통과하는 일반 양식 로그인 페이지는 잘 작동합니다.

새 계정 양식 컨트롤러에서 UsernamePasswordAuthenticationToken을 만들고 SecurityContext에서 수동으로 설정합니다.

SecurityContextHolder.getContext().setAuthentication(authentication);

같은 페이지에서 나중에 사용자가 다음으로 로그인했는지 확인합니다.

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

이것은 인증에서 이전에 설정 한 권한을 반환합니다. 모든 것이 좋습니다.

그러나 내가로드 한 바로 다음 페이지에서이 동일한 코드가 호출되면 인증 토큰은 UserAnonymous입니다.

이전 요청에서 설정 한 인증을 유지하지 않은 이유가 명확하지 않습니다. 이견있는 사람?

  • 세션 ID가 올바르게 설정되지 않은 것과 관련이 있습니까?
  • 어떻게 든 내 인증을 덮어 쓸 수있는 것이 있습니까?
  • 인증을 저장하려면 다른 단계가 필요한가요?
  • 아니면 어떻게 든 단일 요청이 아닌 전체 세션에 걸쳐 인증을 선언하기 위해해야 ​​할 일이 있습니까?

여기서 무슨 일이 일어나고 있는지 확인하는 데 도움이 될 몇 가지 생각을 찾고 있습니다.



2
독자 여러분,이 질문에 대한 답변을 조심하십시오 : SecurityContextHolder.getContext().setAuthentication(authentication). 작동하고 일반적이지만 그렇게하면 만나게 될 심각한 기능적 단점이 있습니다. 자세한 정보를 원하시면, 내 질문과 대답을 참조 stackoverflow.com/questions/47233187/...
염소

답변:


62

나는 당신과 얼마 전에 같은 문제를 겪었습니다. 세부 사항을 기억할 수 없지만 다음 코드는 나를 위해 일했습니다. 이 코드는 Spring Webflow 흐름 내에서 사용되므로 RequestContext 및 ExternalContext 클래스가 사용됩니다. 그러나 가장 관련성이 높은 부분은 doAutoLogin 메서드입니다.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}

2
감사합니다. 코드는 올바른 영역에서 문제를 해결하고 있음을 알리는 데 매우 유용합니다. 내가 담배를 피우는 것 같습니다. 수동 인증 후 새 세션 ID를 생성하고 있지만 이전 세션 ID는 여전히 쿠키에서 식별되고 있습니다. 지금 왜 그런지 알아 내야하는데 적어도 나는 분명히 궤도에 올랐다. 감사!
David Parks

4
이 지침을 따르는 사람은 누구나이 관련 문제를 볼 수 있습니다. stackoverflow.com/questions/4824395/…
David Parks

14
authenticationProvider를받는 방법에 대해 설명해 주시겠습니까?
Vivex

1
@Autowired> \ - @은 당신이 IoC를 통해 주입 얻을 수 있어야 s1moner3d
하르트 무트

1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi

66

다른 전체 솔루션을 찾을 수 없어서 게시 할 것이라고 생각했습니다. 이것은 약간의 해킹 일 수 있지만 위의 문제로 문제를 해결했습니다.

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}

7
+1-도움이되었습니다! SPRING_SECURITY_CONTEXT 업데이트가 누락되었습니다. ...하지만 이것이 얼마나 "더러운"것입니까?
l3dx

12
어디서 authenticationManager오는거야?
Isaac

2
authenticationManager는 @Autowired AuthenticationServiceImpl authenticationManager와 같이 클래스에서 자동 연결됩니다. 또한 XML 구성에 빈 주입이 있어야하므로 Spring은 주입 할 내용을 알고 있습니다.

1
AuthenticationServiceImpl의 구현은 어디에 있습니까? 이 클래스는 무엇을 보유합니까?
Pra_A 2015-04-14

3
새 세션을 만들어야하는 이유는 무엇입니까? SecurityContext가 처리하지 않습니까?
Vlad Manuel Mureșan

17

궁극적으로 문제의 원인을 파악했습니다.

보안 컨텍스트를 수동으로 만들면 세션 개체가 만들어지지 않습니다. 요청이 처리를 완료 할 때만 Spring Security 메커니즘은 세션 객체가 null임을 인식합니다 (요청이 처리 된 후 세션에 보안 컨텍스트를 저장하려고 할 때).

요청이 끝나면 Spring Security는 새로운 세션 객체와 세션 ID를 생성합니다. 그러나이 새 세션 ID는 브라우저에 대한 응답이 이루어진 후 요청이 끝날 때 발생하기 때문에 브라우저에 전달되지 않습니다. 이로 인해 다음 요청에 이전 세션 ID가 포함되어있을 때 새 세션 ID (및 수동으로 로그온 한 사용자를 포함하는 보안 컨텍스트)가 손실됩니다.


4
솔직히 이것은 무엇보다 봄 보안의 디자인 결함처럼 느껴집니다. 이것에 문제가없는 다른 언어로 작성된 많은 프레임 워크가 있지만 Spring Security는 그냥 망가집니다.
chubbsondubs dec

3
그리고 해결책은?
s1moner3d

2
그리고 해결책은 무엇입니까?
Thiago

6

무슨 일이 일어나고 있는지 더 잘 이해하려면 디버그 로깅을 켜십시오.

HTTP 응답에 반환 된 헤더를 확인하기 위해 브라우저 측 디버거를 사용하여 세션 쿠키가 설정되고 있는지 알 수 있습니다. (다른 방법도 있습니다.)

한 가지 가능성은 SpringSecurity가 보안 세션 쿠키를 설정하고 요청 된 다음 페이지에 "https"URL 대신 "http"URL이 있다는 것입니다. (브라우저는 "http"URL에 대한 보안 쿠키를 보내지 않습니다.)


감사합니다. 모두 매우 유용하고 관련있는 제안이었습니다!
David Parks

5

Servlet 2.4의 새로운 필터링 기능은 기본적으로 필터가 애플리케이션 서버에 의한 실제 요청 처리 전후의 요청 흐름에서만 작동 할 수 있다는 제한을 완화합니다. 대신 Servlet 2.4 필터는 이제 모든 디스패치 지점에서 요청 디스패처와 상호 작용할 수 있습니다. 즉, 웹 리소스가 요청을 다른 리소스에 전달할 때 (예 : 동일한 애플리케이션의 JSP 페이지에 요청을 전달하는 서블릿) 대상 리소스가 요청을 처리하기 전에 필터가 작동 할 수 있습니다. 또한 웹 리소스에 다른 웹 리소스 (예 : 여러 다른 JSP 페이지의 출력을 포함하는 JSP 페이지)의 출력 또는 기능이 포함되는 경우 Servlet 2.4 필터가 포함 된 각 리소스의 전후에 작동 할 수 있습니다. .

해당 기능을 켜려면 다음이 필요합니다.

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

RegistrationController

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();

좋은 정보이지만 사용자 이름과 비밀번호를 URL에 입력하는 것은 좋지 않습니다. 1) 이스케이프가 수행되지 않으므로 특수 문자가 포함 된 사용자 이름 또는 암호가 손상되거나 더 나쁜 경우 보안 악용 벡터로 사용될 수 있습니다. 2) URL이 디스크에 기록되는 경우가 많기 때문에 URL의 비밀번호가 좋지 않습니다. 이는 보안에 매우 좋지 않습니다. 모든 비밀번호는 일반 텍스트로 표시됩니다.
염소

1

extjs 응용 프로그램을 테스트하려고 시도했지만 testingAuthenticationToken을 성공적으로 설정 한 후 갑자기 명확한 원인없이 작동이 중지되었습니다.

위의 답변을 얻을 수 없었기 때문에 내 해결책은 테스트 환경에서이 봄을 건너 뛰는 것이 었습니다. 나는 다음과 같이 봄 주위에 솔기를 도입했습니다.

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

여기서 사용자는 사용자 정의 유형입니다.

그런 다음 테스트 코드를 전환하는 옵션이있는 클래스로 래핑합니다.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

테스트 버전은 다음과 같습니다.

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

호출 코드에서 나는 여전히 데이터베이스에서로드 된 적절한 사용자를 사용하고 있습니다.

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

실제로 보안을 사용해야하는 경우에는 분명히 적합하지 않지만 테스트 배포를 위해 보안이없는 설정으로 실행 중입니다. 나는 다른 누군가가 비슷한 상황에 처할 것이라고 생각했습니다. 이것은 이전에 정적 종속성을 조롱하는 데 사용한 패턴입니다. 다른 대안은 래퍼 클래스의 정 적성을 유지할 수 있지만 필요한 경우 CurrentUserAccessor를 클래스에 전달해야하므로 코드의 종속성이 더 명시 적이므로 선호합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.