使用Spring Security和Keycloak进行Spring Websockets身份验证

使用Spring Security和Keycloak进行Spring Websockets身份验证,第1张

使用Spring Security和Keycloak进行Spring Websockets身份验证

对这个问题的建议之后,我能够启用基于令牌的身份验证。这是使其工作的最终代码:

1)首先,创建一个表示JWS auth令牌的类:

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的身份验证器,以对keycloak进行验证。用户是我自己的代表用户的应用类:

@Slf4j@Component@Qualifier("websocket")@AllArgsConstructorpublic 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)基于此要点,通过调用certs端点来验证令牌签名的方法,该类实际上针对keycloak验证了令牌。它返回一个密钥斗篷AccessToken:

@Component@AllArgsConstructorpublic class KeycloakTokenVerifier {  private final KeycloakProperties config;    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");      Deprer urlDeprer = base64.getUrlDeprer();      BigInteger modulus = new BigInteger(1, urlDeprer.depre(modulusbase64));      BigInteger publicExponent = new BigInteger(1, urlDeprer.depre(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的建议完成代码部分:

@Slf4j@Configuration@EnableWebSocketMessageBroker@AllArgsConstructorpublic 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安全性中的任何人开放:

在WebSecurityConfiguration中:

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

在类WebSocketSecurityConfig中:

@Configurationpublic 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。希望它对其他人有帮助!



欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5675907.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存