We have decided to look into using websockets along with our RESTful API for streaming events to and from
the game clients. Since we are already using Spring Security and Keycloak we wanted to be able to re-use
the same accounts and roles that we already have.
We also aren’t using STOMP in this setup as the game client has no clue what STOMP is. For Unity, you might want to
check out Websocket-Sharp
Here’s a very simple breakdown of what we did:
Dependencies for Spring Boot/Spring Security/Keycloak (you don’t have to use Jetty if you are using Tomcat)
dependencies {
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-websocket' , version: '2.3.1.RELEASE'
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-jetty' , version: '2.3.1.RELEASE'
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-actuator' , version: '2.3.1.RELEASE'
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-log4j2' , version: '2.3.1.RELEASE'
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-security' , version: '2.3.1.RELEASE'
implementation group: 'org.keycloak' , name: 'keycloak-spring-boot-starter' , version: '8.0.0'
implementation group: 'org.keycloak' , name: 'keycloak-jetty94-adapter' , version: '8.0.0'
implementation group: 'org.projectlombok' , name: 'lombok' , version: '1.18.12'
}
Spring Security configuration classes
@Configuration
public class ConfigResolver {
@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver () {
return new KeycloakSpringBootConfigResolver ();
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal ( AuthenticationManagerBuilder auth ) {
var provider = keycloakAuthenticationProvider ();
provider . setGrantedAuthoritiesMapper ( grantedAuthoritiesMapper ());
auth . authenticationProvider ( provider );
}
// Makes sure the roles are easier to check later
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper () {
var mapper = new SimpleAuthorityMapper ();
mapper . setConvertToUpperCase ( true );
return mapper ;
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy () {
return new NullAuthenticatedSessionStrategy ();
}
@Override
public void configure ( final HttpSecurity http ) throws Exception {
super . configure ( http );
http . cors (). and (). csrf (). disable ()
// tell Spring Boot to never manage session, Keycloak is being used
. sessionManagement ()
. sessionCreationPolicy ( SessionCreationPolicy . STATELESS )
. and ()
. authorizeRequests ()
// and authorize all the rest
. anyRequest (). authenticated ()
. and ()
. exceptionHandling ()
. authenticationEntryPoint ( authenticationEntryPoint ());
}
}
Spring Websocket configuration
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers ( WebSocketHandlerRegistry registry ) {
registry . addHandler ( new WebSocketHandler (), "/websocket" )
. addInterceptors ( new KeycloakHttpSessionHandshakeInterceptor ());
}
}
Keycloak session handler class
@Slf4j
public class KeycloakHttpSessionHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake ( ServerHttpRequest request ,
ServerHttpResponse response ,
WebSocketHandler wsHandler ,
Map < String , Object > attributes ) throws Exception {
var token = ( KeycloakAuthenticationToken ) request . getPrincipal ();
if ( token == null || ! token . isAuthenticated ()) {
log . debug ( "Invalid Keycloak token, denying access" );
return false ;
}
// you can now check your user granted authorities or add auth objects to the attributes map so you can get
// to them in your websocket handler
// attributes.put("key", "value")
return super . beforeHandshake ( request , response , wsHandler , attributes );
}
@Override
public void afterHandshake ( ServerHttpRequest request ,
ServerHttpResponse response ,
WebSocketHandler wsHandler , Exception ex ) {
super . afterHandshake ( request , response , wsHandler , ex );
}
}
Finally, your websocket handler class with your custom message processing…
@Component
public class WebSocketHandler extends TextWebSocketHandler {
// ...
}
The final class is your normal Spring Websocket Handler class, registered in WebSocketConfig
.