使用框架介绍
spring boot 1.4.3.RELEASE
spring websocket 4.3.5.RELEASE
spring security 4.1.3.RELEASE
sockjs-client 1.0.2
stompjs 2.3.3
项目介绍
由于公司需要使用websocket主动给前端用户推送消息,公司的项目是使用jhipster自动生成的微服务项目,而spring boot本身就集成了websocket,这样我们不用自己处理所有的网络细节代码。我们的项目主要为:
前端 - nodeJS代理 - 后端 - 计算系统(由于我们公司是做云计算的,计算系统是一个底层系统)
项目的主要流程是:
遇到的问题
由于我们使用的是spring security oauth2 来进行认证,而且我们需要吧websocket消息推送给指定用户,这样为了保证websocket和http协议使用的同一套认证系统,我们就必须要把websocket认证集成到spring security中。
第一个问题认证403错误
首先贴出websocket的配置代码
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final static Logger LOG = LoggerFactory.getLogger(WebSocketConfig.class);
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/api/v1/socket/send"); // 推送消息前缀
registry.setApplicationDestinationPrefixes("/api/v1/socket/req"); // 应用请求前缀
registry.setUserDestinationPrefix("/user");//推送用户前缀
}
/**
* 建立连接的端点
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/v1/socket/fallback").setAllowedOrigins("*").withSockJS().setInterceptors(httpSessionHandshakeInterceptor());
}
}
第一次在打开websocket的时候发现三次握手都没有出现就直接报403,最后仔细看错误信息发现错误信息的链接为:/api/v1/socket/fallback/info
,而且该请求使用的是http请求不是websocket请求。最后发现每一个websocket在连接端点之前都会发送一个http GET请求用于保证该服务是存在的。而该请求是程序自动发送的自能自动携带cookie数据,无法发送自定义header。
spring boot自带的认证是:如果/api/v1/socket/fallback/info
该请求通过认证,那么websocket的所有请求以及发送全部自动绑定该认证用户。如果我们想办法让/api/v1/socket/fallback/info
请求通过认证,那么接下来所有的问题都将解决。问题是我们的token是使用自定义header实现的认证。所以该方法不成立,所以只能让websocket自己认证。
为了解决/api/v1/socket/fallback/info
请求的403问题我在安全配置中加入.authorizeRequests().antMatchers("/api/v1/socket/fallback/**").permitAll()
这样第一步判断服务是否存在就解决了,这里离解决websocket的认证问题只是第一步。
第二个问题:如果发送token给后端
stomp 客户端可以直接在websocket请求中加入自定义header,如下:
let socket = new SockJS('/api/v1/socket/fallback')
let stompClient = Stomp.over(socket)
let token = localStorage.getItem('Auth-Token') // eslint-disable-line
stompClient.connect({'Auth-Token': token}, frame => {
stompClient.subscribe('/user/api/v1/socket/send/greetings', data => {
// TODO
})
})
第三个问题:后端如何认证
我们在创建连接的时候前端需要将token发送到后端,现在我们已经将token发送到后端了,但是后端如何接受并处理token得到认证数据呢?带着这个问题开始google吧!http://stackoverflow.com/questions/39422053/spring-4-x-token-based-websocket-sockjs-fallback-authentication
这个链接正好解决了我的问题,
UPDATE 2016-12-13 : the issue referenced below is now marked fixed, so the hack below is no longer necessary which Spring 4.3.5 or above. See https://github.com/spring-projects/spring-framework/blob/master/src/asciidoc/web-websocket.adoc#token-based-authentication.
原来这个问题在4.3.5版本中已经被继承进去了,查看自己的版本是4.3.4,不解释直接升级版本到4.3.5,然后加如代码
@EnableWebSocketMessageBroker
public class MyConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
if (StringUtils.isNotEmpty(jwtToken)) {
UserAuthenticationToken authToken = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authToken);
accessor.setUser(authToken);
}
}
return message;
}
});
}
}
开始测试,发现还是报错MissingCsrfTokenException
,然后开始debug代码,发现代码错误的代码为:
public final class CsrfChannelInterceptor extends ChannelInterceptorAdapter {
private final MessageMatcher<Object> matcher;
public CsrfChannelInterceptor() {
this.matcher = new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
}
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if(!this.matcher.matches(message)) {
return message;
} else {
Map sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
CsrfToken expectedToken = sessionAttributes == null?null:(CsrfToken)sessionAttributes.get(CsrfToken.class.getName());
if(expectedToken == null) { // 在这里为null
throw new MissingCsrfTokenException((String)null); //报错
} else {
String actualTokenValue = SimpMessageHeaderAccessor.wrap(message).getFirstNativeHeader(expectedToken.getHeaderName());
boolean csrfCheckPassed = expectedToken.getToken().equals(actualTokenValue);
if(csrfCheckPassed) {
return message;
} else {
throw new InvalidCsrfTokenException(expectedToken, actualTokenValue);
}
}
}
}
}
仔细查看里面的数据,原来这里是需要在header中存放一些数据,于是乎将configureClientInboundChannel方法修正为:
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
LOG.debug("webSocket token is {}", jwtToken);
if (StringUtils.isNotEmpty(jwtToken)) {
Map sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
sessionAttributes.put(CsrfToken.class.getName(), new DefaultCsrfToken("Auth-Token", "Auth-Token", jwtToken));
UserAuthenticationToken authToken = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authToken);
accessor.setUser(authToken);
}
}
return message;
}
});
}
然后修改websocket安全配置为:
@Configuration
public class WebsocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().permitAll();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
这样websocket 集成spring boot token的认证就搞定了。