使用Spring Security和Keycloak进行Spring Websockets身份验证 [英] Spring Websockets Authentication with Spring Security and Keycloak

查看:551
本文介绍了使用Spring Security和Keycloak进行Spring Websockets身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用Spring Boot(v1.5.10.RELEASE)为Angular编写的应用程序创建一个后端.背面使用弹簧安全带+钥匙斗篷固定.现在,我在SockJS上使用STOMP添加了一个websocket,并希望对其进行保护.我正在尝试遵循 Websocket令牌身份验证,它显示以下代码:

I'm using Spring Boot (v1.5.10.RELEASE) to create a backend for an application written in Angular. The back is secured using spring security + keycloak. Now I'm adding a websocket, using STOMP over SockJS, and wanted to secure it. I'm trying to follow the docs at Websocket Token Authentication, and it shows the following piece of code:

if (StompCommand.CONNECT.equals(accessor.getCommand())) {
  Authentication user = ... ; // access authentication header(s)
  accessor.setUser(user);
}

我可以使用以下方法从客户端检索承载令牌:

I'm able to retrieve the bearer token from the client using:

String token = accessor.getNativeHeader("Authorization").get(0);

我的问题是,如何将其转换为Authentication对象?或者如何从这里继续?因为我总是得到403.这是我的websocket安全配置:

My question is, how can I convert that to an Authentication object? Or how to proceed from here? Because I always get 403. This is my websocket security config:

@Configuration
public class WebSocketSecurityConfig extends 
     AbstractSecurityWebSocketMessageBrokerConfigurer {

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry 
    messages) {
messages.simpDestMatchers("/app/**").authenticated().simpSubscribeDestMatchers("/topic/**").authenticated()
    .anyMessage().denyAll();
}

  @Override
  protected boolean sameOriginDisabled() {
    return true;
  }
}

这是Web安全配置:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class WebSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .authenticationProvider(keycloakAuthenticationProvider())
        .addFilterBefore(keycloakAuthenticationProcessingFilter(), BasicAuthenticationFilter.class)
        .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
        .and()
        .authorizeRequests()
          .requestMatchers(new NegatedRequestMatcher(new AntPathRequestMatcher("/management/**")))
            .hasRole("USER");
  }

  @Override
  protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new NullAuthenticatedSessionStrategy();
  }

  @Bean
  public KeycloakConfigResolver KeycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
  }

}

欢迎任何帮助或想法.

推荐答案

按照的建议,我能够启用基于令牌的身份验证.在

I was able to enable token based authentication, following the recomendations by Raman on this question. Here's the final code to make it work:

1)首先,创建一个表示JWS身份验证令牌的类:

1) First, create a class that represent the JWS auth token:

public class JWSAuthenticationToken extends AbstractAuthenticationToken implements Authentication {

  private static final long serialVersionUID = 1L;

  private String token;
  private User principal;

  public JWSAuthenticationToken(String token) {
    this(token, null, null);
  }

  public JWSAuthenticationToken(String token, User principal, Collection<GrantedAuthority> authorities) {
    super(authorities);
    this.token = token;
    this.principal = principal;
  }

  @Override
  public Object getCredentials() {
    return token;
  }

  @Override
  public Object getPrincipal() {
    return principal;
  }

}

2)然后,创建一个处理JWSToken的身份验证器,并根据密钥斗篷进行验证.用户是我自己的代表用户的应用类:

2) Then, create an authenticator that handles the JWSToken, validating against keycloak. User is my own app class that represents a user:

@Slf4j
@Component
@Qualifier("websocket")
@AllArgsConstructor
public class KeycloakWebSocketAuthManager implements AuthenticationManager {

  private final KeycloakTokenVerifier tokenVerifier;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    JWSAuthenticationToken token = (JWSAuthenticationToken) authentication;
    String tokenString = (String) token.getCredentials();
    try {
      AccessToken accessToken = tokenVerifier.verifyToken(tokenString);
      List<GrantedAuthority> authorities = accessToken.getRealmAccess().getRoles().stream()
          .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
      User user = new User(accessToken.getName(), accessToken.getEmail(), accessToken.getPreferredUsername(),
          accessToken.getRealmAccess().getRoles());
      token = new JWSAuthenticationToken(tokenString, user, authorities);
      token.setAuthenticated(true);
    } catch (VerificationException e) {
      log.debug("Exception authenticating the token {}:", tokenString, e);
      throw new BadCredentialsException("Invalid token");
    }
    return token;
  }

}

3)基于

3) The class that actually validates the token against keycloak by calling the certs endpoint to validate the token signature, based on this gists. It returns a keycloak AccessToken:

@Component
@AllArgsConstructor
public class KeycloakTokenVerifier {

  private final KeycloakProperties config;

  /**
   * Verifies a token against a keycloak instance
   * @param tokenString the string representation of the jws token
   * @return a validated keycloak AccessToken
   * @throws VerificationException when the token is not valid
   */
  public AccessToken verifyToken(String tokenString) throws VerificationException {
    RSATokenVerifier verifier = RSATokenVerifier.create(tokenString);
    PublicKey publicKey = retrievePublicKeyFromCertsEndpoint(verifier.getHeader());
    return verifier.realmUrl(getRealmUrl()).publicKey(publicKey).verify().getToken();
  }

  @SuppressWarnings("unchecked")
  private PublicKey retrievePublicKeyFromCertsEndpoint(JWSHeader jwsHeader) {
    try {
      ObjectMapper om = new ObjectMapper();
      Map<String, Object> certInfos = om.readValue(new URL(getRealmCertsUrl()).openStream(), Map.class);
      List<Map<String, Object>> keys = (List<Map<String, Object>>) certInfos.get("keys");

      Map<String, Object> keyInfo = null;
      for (Map<String, Object> key : keys) {
        String kid = (String) key.get("kid");
        if (jwsHeader.getKeyId().equals(kid)) {
          keyInfo = key;
          break;
        }
      }

      if (keyInfo == null) {
        return null;
      }

      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
      String modulusBase64 = (String) keyInfo.get("n");
      String exponentBase64 = (String) keyInfo.get("e");
      Decoder urlDecoder = Base64.getUrlDecoder();
      BigInteger modulus = new BigInteger(1, urlDecoder.decode(modulusBase64));
      BigInteger publicExponent = new BigInteger(1, urlDecoder.decode(exponentBase64));

      return keyFactory.generatePublic(new RSAPublicKeySpec(modulus, publicExponent));

    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

  public String getRealmUrl() {
    return String.format("%s/realms/%s", config.getAuthServerUrl(), config.getRealm());
  }

  public String getRealmCertsUrl() {
    return getRealmUrl() + "/protocol/openid-connect/certs";
  }

}

4)最后,将身份验证器注入Websocket配置中,并按照spring docs的建议完成代码部分:

4) Finally, inject the authenticator in the Websocket configuration and complete the piece of code as recommended by spring docs:

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
@AllArgsConstructor
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {

  @Qualifier("websocket")
  private AuthenticationManager authenticationManager;

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws-paperless").setAllowedOrigins("*").withSockJS();
  }

  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(new ChannelInterceptorAdapter() {
      @Override
      public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
          Optional.ofNullable(accessor.getNativeHeader("Authorization")).ifPresent(ah -> {
            String bearerToken = ah.get(0).replace("Bearer ", "");
            log.debug("Received bearer token {}", bearerToken);
            JWSAuthenticationToken token = (JWSAuthenticationToken) authenticationManager
                .authenticate(new JWSAuthenticationToken(bearerToken));
            accessor.setUser(token);
          });
        }
        return message;
      }
    });
  }

}

我也更改了我的安全配置.首先,我从Spring Web安全性中排除了WS端点,并且还让连接方法向WebSocket安全性中的任何人开放:

I also changed my security configuration a bit. First, I excluded the WS endpoint from spring web securty, and also let the connection methods open to anyone in the websocket security:

在WebSecurityConfiguration中:

In WebSecurityConfiguration:

  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring()
        .antMatchers("/ws-endpoint/**");
  }

在类WebSocketSecurityConfig中:

And in the class WebSocketSecurityConfig:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

  @Override
  protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages.simpTypeMatchers(CONNECT, UNSUBSCRIBE, DISCONNECT, HEARTBEAT).permitAll()
    .simpDestMatchers("/app/**", "/topic/**").authenticated().simpSubscribeDestMatchers("/topic/**").authenticated()
        .anyMessage().denyAll();
  }

  @Override
  protected boolean sameOriginDisabled() {
    return true;
  }
}

因此,最终结果是:本地网络中的任何人都可以连接到套接字,但是实际上要订阅任何频道,您必须经过身份验证,因此您需要发送带有原始CONNECT消息的Bearer令牌,或者会得到UnauthorizedException.希望它对其他人有帮助!

So the final result is: anybody in the local network can connect to the socket, but to actually subscribe to any channel, you have to be authenticated, so you need to send the Bearer token with the original CONNECT message or you'll get UnauthorizedException. Hope it helps others with this requeriment!

这篇关于使用Spring Security和Keycloak进行Spring Websockets身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆