我正在使用Spring Boot(v1.5.10.RELEASE)为Angular编写的应用程序创建后端。背面使用弹簧安全带+钥匙斗篷固定。现在,我在SockJS上使用STOMP添加了一个websocket,并希望对其进行保护。我正在尝试遵循Websocket令牌身份验证上的文档,它显示以下代码:
if (StompCommand.CONNECT.equals(accessor.getCommand())) { Authentication user = ... ; // access authentication header(s) accessor.setUser(user); }
我可以使用以下方法从客户端检索承载令牌:
String token = accessor.getNativeHeader("Authorization").get(0);
我的问题是,如何将其转换为身份验证对象?或者如何从这里继续?因为我总是得到403。这是我的websocket安全配置:
@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(); } }
欢迎任何帮助或想法。
对这个问题的建议之后,我能够启用基于令牌的身份验证。这是使其工作的最终代码:
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") @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)基于此要点,通过调用certs端点来验证令牌签名的方法,该类实际上针对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的建议完成代码部分:
@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安全性中的任何人开放:
在WebSecurityConfiguration中:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers("/ws-endpoint/**"); }
在类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。希望它对其他人有帮助!