资讯专栏INFORMATION COLUMN

websocket配合spring-security使用token认证

CODING / 2020人阅读

摘要:最后发现每一个在连接端点之前都会发送一个请求用于保证该服务是存在的。而该请求是程序自动发送的自能自动携带数据,无法发送自定义。问题是我们的是使用自定义实现的认证。所以该方法不成立,所以只能让自己认证。

使用框架介绍

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66357.html

相关文章

  • 基于spring-security-oauth2实现单点登录(持续更新)

    摘要:认证服务器和浏览器控制台也没有报错信息。这里简单介绍下如何查阅源码,首先全局搜索自己的配置因为这个地址是认证服务器请求授权的,所以,请求认证的过滤器肯定包含他。未完待续,下一篇介绍资源服务器和认证服务器的集成。 基于spring-security-oauth2-实现单点登录 文章代码地址:链接描述可以下载直接运行,基于springboot2.1.5,springcloud Green...

    zhongmeizhi 评论0 收藏0
  • 基于spring-security-oauth2实现单点登录(持续更新)

    摘要:认证服务器和浏览器控制台也没有报错信息。这里简单介绍下如何查阅源码,首先全局搜索自己的配置因为这个地址是认证服务器请求授权的,所以,请求认证的过滤器肯定包含他。未完待续,下一篇介绍资源服务器和认证服务器的集成。 基于spring-security-oauth2-实现单点登录 文章代码地址:链接描述可以下载直接运行,基于springboot2.1.5,springcloud Green...

    妤锋シ 评论0 收藏0
  • 前后端分离项目 — 基于SpringSecurity OAuth2.0用户认证

    摘要:前言现在的好多项目都是基于移动端以及前后端分离的项目,之前基于的前后端放到一起的项目已经慢慢失宠并淡出我们视线,尤其是当基于的微服务架构以及单页面应用流行起来后,情况更甚。使用生成是什么请自行百度。 1、前言 现在的好多项目都是基于APP移动端以及前后端分离的项目,之前基于Session的前后端放到一起的项目已经慢慢失宠并淡出我们视线,尤其是当基于SpringCloud的微服务架构以及...

    QLQ 评论0 收藏0
  • [译] Elixir、Phoenix、Absinthe、GraphQL、React 和 Apollo

    摘要:对于每个案例,我们插入所需要的测试数据,调用需要测试的函数并对结果作出断言。我们将这个套接字和用户返回以供我们其他的测试使用。 原文地址:Elixir, Phoenix, Absinthe, GraphQL, React, and Apollo: an absurdly deep dive - Part 2 原文作者:Zach Schneider 译文出自:掘金翻译计划 本文永久链接:gi...

    Cympros 评论0 收藏0
  • spring security登录、登出、认证异常返回值的自定义实现

    摘要:在整个学习过程中,我最关心的内容有号几点,其中一点是前后端分离的情况下如何不跳转页面而是返回需要的返回值。登录成功,不跳转页面,返回自定义返回值在官方文档第节,有这么一段描述要进一步控制目标,可以使用属性作为的替代。 在整个学习过程中,我最关心的内容有号几点,其中一点是【前后端分离的情况下如何不跳转页面而是返回需要的返回值】。下面就说一下学习结果,以xml配置位李。 登录成功,不跳转页...

    mushang 评论0 收藏0

发表评论

0条评论

CODING

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<