programing

봄철 웹 소켓 인증 및 인가

sourcejob 2023. 3. 10. 21:29
반응형

봄철 웹 소켓 인증 및 인가

Spring-Security를 사용한 Stomp(Web 소켓) 인증인증을 제대로 구현하기 위해 많은 노력을 기울여 왔습니다.후세를 위해 제가 직접 질문에 답하여 안내하겠습니다.


문제

Spring WebSocket 문서(인증용)는 불분명한 ATM(IMHO)으로 보입니다.그리고 인증과 인가적절하게 처리하는 방법을 이해할 수 없었습니다.


내가 원하는 것

  • 로그인/비밀번호로 사용자를 인증합니다.
  • 익명 사용자가 WebSocket을 통해 접속할 수 없도록 합니다.
  • 권한 부여 계층(사용자, 관리자 등)을 추가합니다.
  • 하고 있다Principal컨트롤러에서 사용할 수 있습니다.

내가 원하지 않는 것

  • HTTP 네고시에이션엔드포인트에서 인증합니다(대부분의 JavaScript 라이브러리는 HTTP 네고시에이션콜과 함께 인증 헤더를 송신하지 않습니다).

위의 매뉴얼이 불분명해 보입니다(IMHO).스프링에서 명확한 매뉴얼이 제공될 때까지 보안 체인의 기능을 이해하기 위해2일간을 소비하지 않도록 하기 위한 보일러 플레이트가 준비되어 있습니다.

롭-레겟은 정말 좋은 시도를 했지만 스프링스 수업을 위조하고 있었고 나는 그렇게 하는 것이 편치 않다.

시작하기 전에 알아야 할 사항:

  • 보안 체인과 http 및 Web Socket 보안 구성은 완전히 독립적입니다.
  • AuthenticationProviderWeb 소켓 인증에는 일절 관여하지 않습니다.
  • 이 경우 HTTP 네고시에이션엔드포인트에서는 인증이 실행되지 않습니다.이는 제가 알고 있는 JavaScripts STOMP(웹소켓) 라이브러리는 HTTP 요청과 함께 필요한 인증 헤더를 보내지 않기 때문입니다.
  • CONNECT 요구로 설정되면 사용자(simpUser)는 웹 소켓세션에 저장되며 이후 메시지에 대한 인증은 필요하지 않습니다.

메이븐 데프

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

Web Socket 구성

다음 구성에서는 간단한 메시지브로커(나중에 보호할 간단한 엔드포인트)를 등록하고 있습니다.

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

Then we'll create a service responsible for authenticating users.
@Component
public class WebSocketAuthenticatorService {
    // This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
    public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String  username, final String password) throws AuthenticationException {
        if (username == null || username.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
        }
        if (password == null || password.trim().isEmpty()) {
            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 하나 이상의 부여가 있어야 합니다.권한, 다른 생성자를 사용하면 스프링이 자동으로 설정됩니다.isAuthenticated = false.


Almost there, now we need to create an Interceptor that will set the `simpUser` header or throw `AuthenticationException` on CONNECT messages.
@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() a를 반환해야 합니다.UsernamePasswordAuthenticationToken스프링 보안 체인의 다른 요소가 이를 테스트합니다.★★★★★★★★★★★의 경우UsernamePasswordAuthenticationToken하지 않고 GrantedAuthority에 실패합니다이 부여되지 으로 설정되기 입니다.권한 부여가 없는 컨스트럭터가 자동으로 설정되기 때문입니다.authenticated = false 는 스프링 보안에 기록되지 않은 중요한 세부 사항입니다.


Finally create two more class to handle respectively Authorization and Authentication.
@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중요하죠. 잊지 마세요. 그러면 우리 요격범이 보안 체인에 가장 먼저 등록될 수 있습니다.

@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;
    }
}

Java 클라이언트 측에서는 다음 테스트 예를 사용합니다.

StompHeaders connectHeaders = new StompHeaders();
connectHeaders.add("login", "test1");
connectHeaders.add("passcode", "test");
stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler());

스프링 인증으로 가는 것은 귀찮은 일입니다.당신은 그것을 간단한 방법으로 할 수 있습니다.웹 필터를 만들고 Authorization 토큰을 직접 읽은 후 인증을 수행합니다.

@Component
public class CustomAuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String authorization = request.getHeader("Authorization");
            if (/*Your condition here*/) {
                // logged
                filterChain.doFilter(servletRequest, servletResponse);
            } else {
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter().write("{\"message\": "\Bad login\"}");
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

다음으로 설정에서 스프링 메커니즘을 사용하여 필터를 정의합니다.

@Configuration
public class SomeConfig {
    @Bean
    public FilterRegistrationBean<CustomAuthenticationFilter> securityFilter(
            CustomAuthenticationFilter customAuthenticationFilter){
        FilterRegistrationBean<CustomAuthenticationFilter> registrationBean
                = new FilterRegistrationBean<>();

        registrationBean.setFilter(customAuthenticationFilter);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

언급URL : https://stackoverflow.com/questions/45405332/websocket-authentication-and-authorization-in-spring

반응형