Spring 使用 Spring 安全和 Keycloak 的 Websockets 身份验证
Spring Websockets Authentication with Spring Security and Keycloak
我正在使用 Spring Boot (v1.5.10.RELEASE) 为使用 Angular 编写的应用程序创建后端。背面使用 spring security + keycloak 进行保护。现在我正在添加一个 websocket,在 SockJS 上使用 STOMP,并希望保护它。我正在尝试按照 Websocket Token Authentication 上的文档进行操作,它显示了以下代码片段:
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();
}
}
欢迎任何帮助或想法。
按照 Raman on 的建议,我能够启用基于令牌的身份验证。这是使其工作的最终代码:
1) 首先,创建一个代表 JWS 身份验证令牌的 class:
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 进行验证。 User是我自己的应用class代表一个用户:
@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) class 根据 this gists 通过调用 certs 端点来验证令牌签名来实际验证针对 keycloak 的令牌。它 returns 一个密钥斗篷 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 配置中注入 authenticator 并完成 spring 文档推荐的代码片段:
@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 网络安全中排除了 WS 端点,并且还让连接方法对网络套接字安全中的任何人开放:
在 WebSecurityConfiguration 中:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/ws-endpoint/**");
}
并且在 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 token 或你会得到 UnauthorizedException。希望它能帮助其他人解决这个问题!
除了 KeycloakTokenVerifier 的部分,我喜欢 adrianmoya 的回答。我改用以下内容:
public class KeycloakWebSocketAuthManager implements AuthenticationManager {
private final KeycloakSpringBootConfigResolver keycloakSpringBootConfigResolver;
@Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
final JWSAuthenticationToken token = (JWSAuthenticationToken) authentication;
final String tokenString = (String) token.getCredentials();
try {
final KeycloakDeployment resolve = keycloakSpringBootConfigResolver.resolve(null);
final AccessToken accessToken = AdapterRSATokenVerifier.verifyToken(tokenString, resolve);
...
}
}
我能够在不使用 Spring 安全性和 SockJS 的情况下执行 websocket authentication/authorization:
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class StompConfiguration implements WebSocketMessageBrokerConfigurer {
private final KeycloakSpringBootProperties configuration;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/stompy"); // prefix for incoming messages in @MessageMapping
config.enableSimpleBroker("/broker"); // enabling broker @SendTo("/broker/blabla")
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp")
.addInterceptors(new StompHandshakeInterceptor(configuration))
.setAllowedOrigins("*");
}
}
握手拦截器:
@Slf4j
@RequiredArgsConstructor
public class StompHandshakeInterceptor implements HandshakeInterceptor {
private final KeycloakSpringBootProperties configuration;
@Override
public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, WebSocketHandler h, Map<String, Object> atts) {
List<String> protocols = req.getHeaders().get("Sec-WebSocket-Protocol");
try {
String token = protocols.get(0).split(", ")[2];
log.debug("Token: " + token);
AdapterTokenVerifier.verifyToken(token, KeycloakDeploymentBuilder.build(configuration));
resp.setStatusCode(HttpStatus.SWITCHING_PROTOCOLS);
log.debug("token valid");
} catch (IndexOutOfBoundsException e) {
resp.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
catch (VerificationException e) {
resp.setStatusCode(HttpStatus.FORBIDDEN);
log.error(e.getMessage());
return false;
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest rq, ServerHttpResponse rp, WebSocketHandler h, @Nullable Exception e) {}
}
Websocket 控制器:
@Controller
public class StompController {
@MessageMapping("/test")
@SendTo("/broker/lol")
public String lol(String message) {
System.out.println("Incoming message: " + message);
return message;
}
}
客户端(javascript):
function connect() {
let protocols = ['v10.stomp', 'v11.stomp'];
protocols.push("KEYCLOAK TOKEN");
const url = "ws://localhost:8080/stomp";
client = Stomp.client(url, protocols);
client.connect(
{},
() => {
console.log("Connection established");
client.subscribe("/broker/lol", function (mes) {
console.log("New message for /broker/lol: " + mes.body);
});
},
error => { console.log("ERROR: " + error); }
);
}
function sendMessage() {
let message = "test message";
if (client) client.send("/stompy/test", {}, message);
}
build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// keycloak
implementation 'org.keycloak:keycloak-spring-boot-starter'
// stomp.js
implementation("org.webjars:webjars-locator-core")
implementation("org.webjars:stomp-websocket:2.3.3")
}
dependencyManagement {
imports {
mavenBom "org.keycloak.bom:keycloak-adapter-bom:$keycloakVersion"
}
}
如您所见,客户端在握手期间已通过身份验证。 HandshakeInterceptor
class 从 Sec-WebSocket-Protocol
header 中提取令牌。不需要 SockJS 或 Spring 安全性。希望这有帮助:)
我正在使用 Spring Boot (v1.5.10.RELEASE) 为使用 Angular 编写的应用程序创建后端。背面使用 spring security + keycloak 进行保护。现在我正在添加一个 websocket,在 SockJS 上使用 STOMP,并希望保护它。我正在尝试按照 Websocket Token Authentication 上的文档进行操作,它显示了以下代码片段:
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();
}
}
欢迎任何帮助或想法。
按照 Raman on
1) 首先,创建一个代表 JWS 身份验证令牌的 class:
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 进行验证。 User是我自己的应用class代表一个用户:
@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) class 根据 this gists 通过调用 certs 端点来验证令牌签名来实际验证针对 keycloak 的令牌。它 returns 一个密钥斗篷 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 配置中注入 authenticator 并完成 spring 文档推荐的代码片段:
@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 网络安全中排除了 WS 端点,并且还让连接方法对网络套接字安全中的任何人开放:
在 WebSecurityConfiguration 中:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/ws-endpoint/**");
}
并且在 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 token 或你会得到 UnauthorizedException。希望它能帮助其他人解决这个问题!
除了 KeycloakTokenVerifier 的部分,我喜欢 adrianmoya 的回答。我改用以下内容:
public class KeycloakWebSocketAuthManager implements AuthenticationManager {
private final KeycloakSpringBootConfigResolver keycloakSpringBootConfigResolver;
@Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
final JWSAuthenticationToken token = (JWSAuthenticationToken) authentication;
final String tokenString = (String) token.getCredentials();
try {
final KeycloakDeployment resolve = keycloakSpringBootConfigResolver.resolve(null);
final AccessToken accessToken = AdapterRSATokenVerifier.verifyToken(tokenString, resolve);
...
}
}
我能够在不使用 Spring 安全性和 SockJS 的情况下执行 websocket authentication/authorization:
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class StompConfiguration implements WebSocketMessageBrokerConfigurer {
private final KeycloakSpringBootProperties configuration;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/stompy"); // prefix for incoming messages in @MessageMapping
config.enableSimpleBroker("/broker"); // enabling broker @SendTo("/broker/blabla")
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp")
.addInterceptors(new StompHandshakeInterceptor(configuration))
.setAllowedOrigins("*");
}
}
握手拦截器:
@Slf4j
@RequiredArgsConstructor
public class StompHandshakeInterceptor implements HandshakeInterceptor {
private final KeycloakSpringBootProperties configuration;
@Override
public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, WebSocketHandler h, Map<String, Object> atts) {
List<String> protocols = req.getHeaders().get("Sec-WebSocket-Protocol");
try {
String token = protocols.get(0).split(", ")[2];
log.debug("Token: " + token);
AdapterTokenVerifier.verifyToken(token, KeycloakDeploymentBuilder.build(configuration));
resp.setStatusCode(HttpStatus.SWITCHING_PROTOCOLS);
log.debug("token valid");
} catch (IndexOutOfBoundsException e) {
resp.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
catch (VerificationException e) {
resp.setStatusCode(HttpStatus.FORBIDDEN);
log.error(e.getMessage());
return false;
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest rq, ServerHttpResponse rp, WebSocketHandler h, @Nullable Exception e) {}
}
Websocket 控制器:
@Controller
public class StompController {
@MessageMapping("/test")
@SendTo("/broker/lol")
public String lol(String message) {
System.out.println("Incoming message: " + message);
return message;
}
}
客户端(javascript):
function connect() {
let protocols = ['v10.stomp', 'v11.stomp'];
protocols.push("KEYCLOAK TOKEN");
const url = "ws://localhost:8080/stomp";
client = Stomp.client(url, protocols);
client.connect(
{},
() => {
console.log("Connection established");
client.subscribe("/broker/lol", function (mes) {
console.log("New message for /broker/lol: " + mes.body);
});
},
error => { console.log("ERROR: " + error); }
);
}
function sendMessage() {
let message = "test message";
if (client) client.send("/stompy/test", {}, message);
}
build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// keycloak
implementation 'org.keycloak:keycloak-spring-boot-starter'
// stomp.js
implementation("org.webjars:webjars-locator-core")
implementation("org.webjars:stomp-websocket:2.3.3")
}
dependencyManagement {
imports {
mavenBom "org.keycloak.bom:keycloak-adapter-bom:$keycloakVersion"
}
}
如您所见,客户端在握手期间已通过身份验证。 HandshakeInterceptor
class 从 Sec-WebSocket-Protocol
header 中提取令牌。不需要 SockJS 或 Spring 安全性。希望这有帮助:)