Spring을 통한 RESTful 인증


262

문제:
민감한 정보가 포함 된 Spring MVC 기반 RESTful API가 있습니다. API는 보안되어야하지만 각 요청과 함께 사용자의 자격 증명 (사용자 / 패스 콤보)을 보내는 것은 바람직하지 않습니다. REST 지침 및 내부 비즈니스 요구 사항에 따라 서버는 상태 비 저장 상태로 유지되어야합니다. API는 매시업 스타일의 접근 방식으로 다른 서버에서 사용합니다.

요구 사항 :

  • 클라이언트는 .../authenticate자격 증명을 사용 하여 (보호되지 않은 URL)을 요청합니다 . 서버는 서버가 향후 요청을 확인하고 상태 비 저장 상태를 유지하기에 충분한 정보가 포함 된 보안 토큰을 반환합니다. 이것은 Spring Security의 Remember-Me Token 과 동일한 정보로 구성 될 수 있습니다 .

  • 클라이언트는 이전에 얻은 토큰을 쿼리 매개 변수 (또는 덜 바람직하게는 HTTP 요청 헤더)로 추가하여 다양한 (보호 된) URL에 후속 요청을합니다.

  • 클라이언트는 쿠키를 저장할 것으로 예상 할 수 없습니다.

  • Spring을 이미 사용하고 있으므로 솔루션은 Spring Security를 ​​사용해야합니다.

우리는이 일을하려고 노력하면서 벽에 머리를 대고 있었기 때문에 누군가가 이미이 문제를 해결했을 것입니다.

위의 시나리오에서 이러한 특정 요구를 어떻게 해결할 수 있습니까?


49
안녕하세요 크리스, 쿼리 매개 변수에 해당 토큰을 전달하는 것이 가장 좋습니다. HTTPS 또는 HTTP에 관계없이 로그에 표시됩니다. 헤더가 더 안전 할 것입니다. 참고로 그래도 좋은 질문입니다. +1
jmort253 1

1
무국적자에 대한 이해는 무엇입니까? 귀하의 토큰 요구 사항은 Stateless에 대한 나의 이해와 충돌합니다. HTTP 인증 응답은 나에게 유일한 Stateless 구현으로 보입니다.
Markus Malkusch

9
@MarkusMalkusch Stateless는 주어진 클라이언트와의 사전 통신에 대한 서버의 지식을 나타냅니다. HTTP는 정의상 상태가 없으며 세션 쿠키는 상태를 유지합니다. 토큰의 수명 (및 해당 문제의 출처)은 관련이 없습니다. 서버는 서버가 유효하고 사용자에게 다시 연결될 수 있다는 것만 걱정합니다 (세션 아님). 따라서 식별 토큰을 전달해도 상태 저장을 방해하지 않습니다.
Chris Cashwell

1
@ChrisCashwell 클라이언트가 토큰을 스푸핑 / 생성하지 않도록하려면 어떻게해야합니까? 서버 측에서 개인 키를 사용하여 토큰을 암호화하고 클라이언트에 제공 한 다음 이후에 요청할 때 동일한 키를 사용하여 암호를 해독합니까? 분명히 Base64 또는 다른 난독 화로는 충분하지 않습니다. 이 토큰의 "유효성 검증"기술에 대해 자세히 설명 할 수 있습니까?
Craig Otis

6
이 날짜는 구식이며 2 년 넘게 코드를 건드 리거나 업데이트하지 않았지만 이러한 개념을 더욱 확장하기 위해 요점을 만들었습니다. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris

답변:


190

우리는 OP에 설명 된대로 정확하게 작동하도록 노력했으며 다른 누군가가 솔루션을 사용할 수 있기를 바랍니다. 우리가 한 일은 다음과 같습니다.

다음과 같이 보안 컨텍스트를 설정하십시오.

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

보시다시피, 우리는 custom을 만들었습니다. 이 필터는 AuthenticationEntryPoint기본적으로 401 Unauthorized요청이 필터 체인에서 인증되지 않은 경우를 반환합니다 AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

분명히, TokenUtils몇 가지 특유의 (대부분의 경우에만) 코드를 포함하며 쉽게 공유 할 수 없습니다. 인터페이스는 다음과 같습니다.

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

좋은 출발을해야합니다. 행복한 코딩. :)


토큰이 요청과 함께 전송 될 때 토큰을 인증해야합니까? 사용자 이름 정보를 직접 가져 와서 현재 컨텍스트 / 요청에 설정하는 것은 어떻습니까?
Fisher

1
@Spring 나는 어디에도 저장하지 않습니다 ... 토큰의 아이디어는 모든 요청과 함께 전달되어야하며 유효성을 결정하기 위해 (부분적으로) 해체 될 수 있다는 것입니다 (따라서 validate(...)방법). 서버가 상태를 유지하기를 원하기 때문에 중요합니다. Spring을 사용할 필요 없이이 접근법을 사용할 수 있다고 생각합니다.
Chris

1
클라이언트가 브라우저 인 경우 토큰을 어떻게 저장할 수 있습니까? 또는 각 요청에 대한 인증을 다시 실행해야합니까?
beginner_

2
좋은 팁. @ChrisCashwell-찾을 수없는 부분은 사용자 자격 증명의 유효성을 검사하고 토큰을 다시 보내는 곳입니다. 나는 그것이 / authenticate 엔드 포인트의 어딘가에 있어야한다고 생각합니다. 내가 맞아? 그렇지 않다면 / authenticate의 목표는 무엇입니까?
Yonatan Maman

3
AuthenticationManager 안에 무엇이 있습니까?
MoienGK

25

다이제스트 액세스 인증을 고려할 수 있습니다 . 기본적으로 프로토콜은 다음과 같습니다.

  1. 클라이언트로부터 요청
  2. 서버가 고유 한 nonce 문자열로 응답
  3. 클라이언트는 nonce로 해시 된 사용자 이름 및 비밀번호 (및 기타 값) md5를 제공합니다. 이 해시는 HA1이라고합니다
  4. 그런 다음 서버는 고객의 신원을 확인하고 요청 된 자료를 제공 할 수 있습니다
  5. 서버가 새로운 nonce를 제공 ​​할 때까지 nonce와의 통신을 계속할 수 있습니다 (카운터는 재생 공격을 제거하는 데 사용됨)

이 모든 통신은 jmort253이 지적한 것처럼 일반적으로 URL 매개 변수로 민감한 자료를 통신하는 것보다 더 안전한 헤더를 통해 이루어집니다.

다이제스트 액세스 인증은 Spring Security에서 지원합니다 . 문서에서 클라이언트의 일반 텍스트 비밀번호에 액세스해야한다고 말하지만 클라이언트에 대한 HA1 해시가 있으면 성공적으로 인증 할 수 있습니다 .


1
이것이 가능한 접근법이지만, 토큰을 검색하기 위해 수행해야하는 몇 차례의 왕복은 약간 바람직하지 않습니다.
Chris Cashwell

클라이언트가 HTTP 인증 사양을 따르는 경우 해당 라운드 트립은 첫 번째 호출 및 5. 발생시에만 발생합니다.
Markus Malkusch

5

정보를 전달하는 토큰과 관련하여 JSON 웹 토큰 ( http://jwt.io )은 훌륭한 기술입니다. 주요 개념은 토큰에 정보 요소 (클레임)를 포함시킨 다음 전체 토큰에 서명하여 유효성 검사 측에서 클레임이 실제로 신뢰할 수 있는지 확인할 수 있도록하는 것입니다.

이 Java 구현을 사용합니다 : https://bitbucket.org/b_c/jose4j/wiki/Home

Spring 모듈 (spring-security-jwt)도 있지만, 그것이 지원하는 것을 조사하지 않았습니다.


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