简体   繁体   English

Spring-websockets:Spring 安全授权在 websockets 中不起作用

[英]Spring-websockets : Spring security authorization not working inside websockets

I am working on a Spring-MVC application in which we have Spring-security for authentication and authorization.我正在开发一个 Spring-MVC 应用程序,其中我们有用于身份验证和授权的 Spring-security。 We are working on migrating to Spring websockets, but we are having an issue with getting the authenticated user inside a websocket connection.我们正在努力迁移到 Spring websockets,但是我们遇到了在 websocket 连接中获取经过身份验证的用户的问题。 The security context simply doesn't exist in the websocket connection, but works fine with regular HTTP. websocket 连接中根本不存在安全上下文,但在常规 HTTP 中可以正常工作。 What are we doing wrong?我们做错了什么?

WebsocketConfig :网络套接字配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app").withSockJS();
    }
}

In the controller below, we are trying to get the currently authenticated user and it's always null在下面的控制器中,我们试图获取当前经过身份验证的用户,它始终为空

@Controller
public class OnlineStatusController extends MasterController{

    @MessageMapping("/onlinestatus")
    public void onlineStatus(String status) {
        Person user = this.personService.getCurrentlyAuthenticatedUser();
        if(user!=null){
            this.chatService.setOnlineStatus(status, user.getId());
        }
    }
}

security-applicationContext.xml :安全-applicationContext.xml :

  <security:http pattern="/resources/**" security="none"/>
    <security:http pattern="/org/**" security="none"/>
    <security:http pattern="/jquery/**" security="none"/>
    <security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
        <security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
                             login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
                             always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
        <security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
                              token-validity-seconds="1209600" data-source-ref="dataSource"/>
        <security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
        <security:csrf disabled="true"/>
        <security:intercept-url pattern="/cometd/**" access="permitAll" />
        <security:intercept-url pattern="/app/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!--        <security:intercept-url pattern="/**" requires-channel="https"/>-->
        <security:port-mappings>
            <security:port-mapping http="80" https="443"/>
        </security:port-mappings>
        <security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
        <security:session-management session-fixation-protection="newSession">
            <security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
        </security:session-management>
    </security:http>

I remember stumbling across the very same problem in a project I was working on.我记得在我正在从事的一个项目中遇到了同样的问题。 As I could not figure out the solution using the Spring documentation - and other answers on Stack Overflow were not working for me - I ended up creating a workaround.由于我无法使用 Spring 文档找出解决方案 - Stack Overflow 上的其他答案对我不起作用 - 我最终创建了一个解决方法。

The trick is essentially to force the application to authenticate the user on a WebSocket connection request.技巧本质上是强制应用程序在 WebSocket 连接请求上对用户进行身份验证。 To do that, you need a class which intercepts such events and then once you have control of that, you can call your authentication logic.为此,您需要一个拦截此类事件的类,然后一旦您控制了它,就可以调用您的身份验证逻辑。

Create a class which implements Spring's ChannelInterceptorAdapter .创建一个实现 Spring 的ChannelInterceptorAdapter Inside this class, you can inject any beans you need to perform the actual authentication.在这个类中,您可以注入执行实际身份验证所需的任何 bean。 My example uses basic auth:我的示例使用基本身份验证:

@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {

@Autowired
private DaoAuthenticationProvider userAuthenticationProvider;

@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {

    final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
    StompCommand cmd = accessor.getCommand();

    if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
        Authentication authenticatedUser = null;
        String authorization = accessor.getFirstNativeHeader("Authorization:);
        String credentialsToDecode = authorization.split("\\s")[1];
        String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
        String[] credentialsDecodedSplit = credentialsDecoded.split(":");
        final String username = credentialsDecodedSplit[0];
        final String password = credentialsDecodedSplit[1];
        authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        if (authenticatedUser == null) {
            throw new AccessDeniedException();
        } 
        SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
        accessor.setUser(authenticatedUser);    
 }
    return message;
 }

Then, in your WebSocketConfig class, you need to register your interceptor.然后,在您的WebSocketConfig类中,您需要注册您的拦截器。 Add the above class as a bean and register it.将上述类添加为 bean 并注册它。 After these changes, your class would look like this:进行这些更改后,您的类将如下所示:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Autowired
private WebSocketAuthInterceptorAdapter authInterceptorAdapter;


@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/app").withSockJS();
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.setInterceptors(authInterceptorAdapter);
    super.configureClientInboundChannel(registration);
}
}

Obviously, the details of the authentication logic are up to you.显然,身份验证逻辑的细节取决于您。 You can call a JWT service or whatever you are using.您可以调用 JWT 服务或您正在使用的任何服务。

If you are using SockJS + Stomp and configured your security correctly, you should be able to connect via regular username/pw authenticator like @AlgorithmFromHell and do如果您使用的是 SockJS + Stomp 并正确配置了您的安全性,您应该能够通过常规用户名/密码验证器(如@AlgorithmFromHell)进行连接并执行

 accessor.setUser(authentication.getPrincipal()) // stomp header accessor

You can also connect via http://{END_POINT}/access_token={ACCESS_TOKEN}.您也可以通过 http://{END_POINT}/access_token={ACCESS_TOKEN} 进行连接。 Spring security should be able to pick it and do loadAuthentication(access_token) via ResourceServerTokenServices. Spring security 应该能够选择它并通过 ResourceServerTokenServices 执行 loadAuthentication(access_token)。 When this is done, you can get your principal by adding this to your impl of AbstractSessionWebSocketMessageBrokerConfigurer or WebSocketMessageBrokerConfigurer.完成后,您可以通过将其添加到 AbstractSessionWebSocketMessageBrokerConfigurer 或 WebSocketMessageBrokerConfigurer 的实现来获取您的委托人。 When doing this, for some reason, the loaded Pricipal is saved in "simpUser" header instead.这样做时,出于某种原因,加载的 Pricipal 保存在“simpUser”标头中。

 @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptor() { @Override public Message<?> preSend(final Message<?> message, final MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser"); accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null); } } return message; } }); }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM