복붙노트

[SPRING] Spring의 Websocket 인증 및 권한 부여

SPRING

Spring의 Websocket 인증 및 권한 부여

Spring Security로 Stomp (websocket) 인증 및 권한 부여를 제대로 구현하기 위해 많은 노력을 기울였습니다. 후손을 위해 나는 가이드를 제공하기 위해 내 자신의 질문에 대답 할 것이다.

Spring WebSocket 문서 (인증 용)는 ATM (IMHO)이 불분명합니다. 그리고 난 제대로 인증 및 권한을 처리하는 방법을 이해할 수 없었다.

해결법

  1. ==============================

    1.위에서 언급했듯이 Spring이 몇 가지 분명한 문서를 제공하기까지는 문서 (ATM)가 명확하지 않습니다. 보안 체인이하는 일을 이해하려고 노력하는 데 2 ​​일을 소비하지 않도록하는 보일러 플레이트가 있습니다.

    위에서 언급했듯이 Spring이 몇 가지 분명한 문서를 제공하기까지는 문서 (ATM)가 명확하지 않습니다. 보안 체인이하는 일을 이해하려고 노력하는 데 2 ​​일을 소비하지 않도록하는 보일러 플레이트가 있습니다.

    정말 멋진 시도가 Rob-Leggett에 의해 만들어졌지만, 그는 스프링 스쿨 클래스를 포크로 찍고 있었고 편안하게 느끼지 않습니다.

    알아 둘 사항 :

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-messaging</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-messaging</artifactId>
    </dependency>
    

    아래의 설정은 간단한 메시지 브로커를 등록합니다 (인증이나 권한 부여와 아무 관계가 없습니다).

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
        @Override
        public void configureMessageBroker(final MessageBrokerRegistry config) {
            // These are endpoints the client can subscribes to.
            config.enableSimpleBroker("/queue/topic");
            // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(final StompEndpointRegistry registry) {
            // Handshake endpoint
            registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
        }
    }
    

    Stomp 프로토콜은 첫 번째 HTTP 요청에 의존하기 때문에 Stomp 핸드 셰이크 엔드 포인트에 대한 HTTP 호출을 인증해야합니다.

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            // This is not for websocket authorization, and this should most likely not be altered.
            http
                    .httpBasic().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    .authorizeRequests().antMatchers("/stomp").permitAll()
                    .anyRequest().denyAll();
        }
    }
    

    그런 다음 사용자 인증을 담당하는 서비스를 만듭니다.

    @Component
    public class WebSocketAuthenticatorService {
        // This method MSUT return a UsernamePasswordAuthenticationToken, another component in the security chain is testing it with 'instanceof'
        public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String  username, final String password) throws AuthenticationException {
            if (username == null || username.trim().length()) {
                throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
            }
            if (password == null || password.trim().length()) {
                throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
            }
            // Add your own logic for retrieving user in fetchUserFromDb()
            if (fetchUserFromDb(username, password) == null) {
                throw new BadCredentialsException("Bad credentials for user " + username);
            }
    
            // null credentials, we do not pass the password along
            return new UsernamePasswordAuthenticationToken(
                    username,
                    null,
                    Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
            );
        }
    }
    

    참고 : UsernamePasswordAuthenticationToken은 GrantedAuthorities를 가져야하며, 다른 생성자를 사용하면 Spring은 isAuthenticated = false를 자동으로 설정합니다.

    거의 대부분, 이제 우리는 simpUser 헤더를 설정하거나 CONNECT 메시지에서 AuthenticationException을 throw 할 인터셉터를 생성해야합니다.

    @Component
    public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
        private static final String USERNAME_HEADER = "login";
        private static final String PASSWORD_HEADER = "passcode";
        private final WebSocketAuthenticatorService webSocketAuthenticatorService;
    
        @Inject
        public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
            this.webSocketAuthenticatorService = webSocketAuthenticatorService;
        }
    
        @Override
        public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
            final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
    
            if (StompCommand.CONNECT == accessor.getCommand()) {
                final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
                final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
    
                final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
    
                accessor.setUser(user);
            }
            return message;
        }
    }
    

    preSend ()는 UsernamePasswordAuthenticationToken을 반환해야하며, 스프링 보안 체인의 또 다른 요소는이를 테스트해야합니다. 참고 : GrantedAuthority를 ​​통과하지 않고 UsernamePasswordAuthenticationToken을 구축 한 경우 인증이 실패합니다. 권한이 부여되지 않은 생성자가 authenticated = false를 자동으로 설정했기 때문에 인증에 실패합니다. 이는 봄 보안에 문서화되지 않은 중요한 세부 사항입니다.

    마지막으로 인증 및 인증 각각을 처리 할 두 개의 클래스를 추가로 작성하십시오.

    @Configuration
    @Order(Ordered.HIGHEST_PRECEDENCE + 99)
    public class WebSocketAuthenticationSecurityConfig extends  WebSocketMessageBrokerConfigurer {
        @Inject
        private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
    
        @Override
        public void registerStompEndpoints(final StompEndpointRegistry registry) {
            // Endpoints are already registered on WebSocketConfig, no need to add more.
        }
    
        @Override
        public void configureClientInboundChannel(final ChannelRegistration registration) {
            registration.setInterceptors(authChannelInterceptorAdapter);
        }
    
    }
    

    참고 : @Order는 CRUCIAL입니다. 잊지 말고, 우리의 인터셉터를 보안 체인에 먼저 등록 할 수 있습니다.

    @Configuration
    public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
        @Override
        protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
            // You can customize your authorization mapping here.
            messages.anyMessage().authenticated();
        }
    
        // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
        @Override
        protected boolean sameOriginDisabled() {
            return true;
        }
    }
    

    행운을 빕니다 !

  2. ==============================

    2.자바 클라이언트 측에서이 테스트 된 예제를 사용한다.

    자바 클라이언트 측에서이 테스트 된 예제를 사용한다.

    StompHeaders connectHeaders = new StompHeaders();
    connectHeaders.add("login", "test1");
    connectHeaders.add("passcode", "test");
    stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler);
    
  3. from https://stackoverflow.com/questions/45405332/websocket-authentication-and-authorization-in-spring by cc-by-sa and MIT license