websocket配合spring-security使用token认证

使用框架介绍

  • 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的认证就搞定了。


我的java进阶之旅
记录我的进阶之旅
82 声望
6 粉丝
0 条评论
推荐阅读
API编写规范
为什么设计api详情规范 主要是为了保证公司的小伙伴写出的api尽量少出bug,加快研发速度。 api 设计规范 绝大部分api都请务必加入用户id限定,主要为了防止某个用户可以请求所有的数据库数据; 管理员api,必须加...

刘祖明1阅读 3.7k

Java 编译器 javac 及 Lombok 实现原理解析
javac 是 Java 代码的编译器12,初学 Java 的时候就应该接触过。本文整理一些 javac 相关的高级用法。Lombok 库,大家平常一直在使用,但可能并不知道实现原理解析,其实 Lombok 实现上依赖的是 Java 编译器的注...

nullwy10阅读 5.9k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.8k

封面图
spring boot 锁
由于当前的项目中由于多线程操作同一个实体,会出现数据覆盖的问题,后保存的实体把先保存的实体的数据给覆盖了。于是查找了锁的实现的几种方式。但写到最后发现,其实自己可以写sql 更新需要更新的字段即可,这...

weiewiyi3阅读 9.1k

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 1.8k评论 2

封面图
Redis 发布订阅模式:原理拆解并实现一个消息队列
“65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?““那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆宣传,暴击单身狗。”像这种 65 哥通过朋...

码哥字节5阅读 1.1k

封面图
NB的Github项目,看到最后一个我惊呆了!
最近看到不少好玩的、实用的 Github 项目,就来给大家推荐一把。中国制霸生成器最近在朋友圈非常火的一个小网站,可以在线标记 居住、短居、游玩、出差、路过 标记后可生成图片进行社区分享,标记过的信息会记录...

艾小仙5阅读 1.5k评论 1

82 声望
6 粉丝
宣传栏