我们已经安全地为我们的项目成功开发了webosocket + stomp + rabbitmq。尽管我们在解决以下情况时遇到一些问题,但它工作正常:此websocket的工作流程如下:
第一个用户订阅了正常工作的websocket端点
在获得用户令牌授权后,用户尝试订阅以下端点'/ user / queue /'+ chatRoomId +“ .messages”。这里chatroomId定义了哪个聊天室用户连接到,这也可以正常工作,但是在这里用户可以连接任何尚未在后端验证的chatroomid我们要解决的大问题。
stompClient.subscribe('/ user / queue /'+ chatRoomId +'.messages',来电消息);
我的问题是,当任何用户尝试订阅此端点时,我该如何验证他们?我的意思是有什么办法可以处理每个特定的订阅
这是我们的前端代码。如果您需要完整的页面,我会上传它
function connect() {
socket = new SockJS('http://localhost:9600/wsss/messages');
stompClient = Stomp.over(socket);
// var stompClient = Stomp.client("ws://localhost:9600/ws/messages");
// stompClient.connect({ 'chat_id' : chatRoomId,
// 'X-Authorization' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0Iiwic2NvcGVzIjoiUk9MRV9BRE1JTiIsImVtYWlsIjoiYWRtaW5AZ21haWwuY29tIiwiaWF0IjoxNTc5MDgxMzg5LCJleHAiOjE1ODE2NzMzODl9.H3mnti0ZNtH6uLe-sOfrr5jzwssvGNcBiHGg-nUQ6xY' },
// stompSuccess, stompFailure);
stompClient.connect({ 'chatRoomId' : chatRoomId,
'login' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyODg5Iiwic2NvcGVzIjoiUk9MRV9VU0VSLFJPTEVfTU9ERVJBVE9SIiwiaWF0IjoxNTgyMDMxMDA0LCJleHAiOjE1ODQ2MjMwMDR9.NGAAed4R46FgrtgyDmrLSrmd-o3tkqbF60vOg8vAWYg' },
stompSuccess, stompFailure);
}
function stompSuccess(frame) {
enableInputMessage();
successMessage("Your WebSocket connection was successfuly established!");
console.log(frame);
stompClient.subscribe('/user/queue/' + chatRoomId + '.messages', incomingMessages);
stompClient.subscribe('/topic/notification', incomingNotificationMessage);
// stompClient.subscribe('/app/join/notification', incomingNotificationMessage);
}
这是我在后端使用的代码
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
class WebSocketConfig @Autowired constructor(
val jwtTokenUtil: TokenProvider
) : WebSocketMessageBrokerConfigurer {
@Autowired
@Resource(name = "userService")
private val userDetailsService: UserDetailsService? = null
@Autowired
private lateinit var authenticationManager: AuthenticationManager
@Value("\${spring.rabbitmq.username}")
private val userName: String? = null
@Value("\${spring.rabbitmq.password}")
private val password: String? = null
@Value("\${spring.rabbitmq.host}")
private val host: String? = null
@Value("\${spring.rabbitmq.port}")
private val port: Int = 0
@Value("\${endpoint}")
private val endpoint: String? = null
@Value("\${destination.prefix}")
private val destinationPrefix: String? = null
@Value("\${stomp.broker.relay}")
private val stompBrokerRelay: String? = null
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableStompBrokerRelay("/queue/", "/topic/")
.setRelayHost(host!!)
.setRelayPort(port)
.setSystemLogin(userName!!)
.setSystemPasscode(password!!)
config.setApplicationDestinationPrefixes(destinationPrefix!!)
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/websocket").setAllowedOrigins("*").setAllowedOrigins("*")
registry.addEndpoint("/websocket/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*")
registry.addEndpoint("/wsss/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*").withSockJS()
}
@Bean
fun customHttpSessionHandshakeInterceptor(): CustomHttpSessionHandshakeInterceptor {
return CustomHttpSessionHandshakeInterceptor()
}
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(object : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
val authorization = accessor.getNativeHeader("login")
println("X-Authorization: {$authorization}")
val authToken = authorization!![0].split(" ")[1]
val username = jwtTokenUtil.getUsernameFromToken(authToken)
if (username != null) {
if(username.contains("@")) {
val userDetails = userDetailsService!!.loadUserByUsername(username)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
val authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().authentication, userDetails)
accessor.user = authentication
}
} else {
val authorities = jwtTokenUtil.getAuthoritiesFromToken(authToken)
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(username, "", authorities)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
accessor.user = authentication
}
}
}
return message
}
})
}
}
这里是事件处理程序
@Component
class WebSocketEvents {
@EventListener
fun handleSessionConnected(event: SessionConnectEvent) {
val headers = SimpMessageHeaderAccessor.wrap(event.message)
if ( headers.getNativeHeader("chatRoomId") != null && headers.getNativeHeader("chatRoomId")!!.isNotEmpty()){
val chatId = headers.getNativeHeader("chatRoomId")!![0]
if (headers.sessionAttributes != null)
headers.sessionAttributes!!["chatRoomId"] = chatId
}
}
@EventListener
fun handleSessionDisconnect(event: SessionDisconnectEvent) {
val headers = SimpMessageHeaderAccessor.wrap(event.message)
val chatRoomId = headers.sessionAttributes!!["chatRoomId"].toString()
}
}
到目前为止,我已经尝试过:如上所示,当用户首次连接到websocket端点http://localhost:9600/wsss/messages时,它正在发送令牌和聊天室ID(标题),而我正在事件监听器组件中通过将chatroomid重置为标题属性来处理此问题。我真正需要做的是在用户订阅此特定目的时获取聊天室ID,并验证他是否属于此聊天室,如果是,请授予他权限。如果不返回错误,让他加入聊天我真的很感谢任何想法或解决方法!
我花了几天时间寻找答案,但没有找到答案,所以我自己弄清楚了。这是我针对此问题的解决方案,尽管还不完整。
我已经创建了用于处理所有连接类型的单独的拦截器类,就像在捕获订阅命令时所做的那样。所以我想到了,为什么不使用Subscribe命令来监听用户采取行动并做出正确回应。例如这样
@Component
class WebSocketTopicHandlerInterceptor constructor() : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
val authorization = accessor.getNativeHeader("login").apply { if (isNullOrEmpty()) throw LoggedError(AuthorizationException()) }
val authToken = authorization!![0].split(" ").apply { if (size <= 1) throw LoggedError(InvalidTokenException("Token is not valid")) }[1]
val username = jwtTokenUtil.getUsernameFromToken(authToken)
//DO YOUR AUTHENTICATION HERE
}
if (StompCommand.SUBSCRIBE == accessor.command) {
val destination = accessor.destination
if (destination.isNullOrBlank()) throw LoggedError(CustomBadRequestException("Subscription destionation cannot be null! U DUMB IDIOT!"))
val chatPattern = "/user/queue/+[a-zA-Z0-9-]+.messages".toRegex()
val notificationPattern = "/topic/notification".toRegex()
if (chatPattern.matches(accessor.destination!!)) println("working")
// FINDING OUT WHERE USER IS TRYING TO SUBSCRIBE ALL ROUTING LOGIC GOES HERE...
when {
chatPattern.matches(destination) -> {
//do your all logic here
}
notificationPattern.matches(destination) -> {
//do your all logic here
}
}
}
return message
}
}
是什么让我不知道,让我知道我非常乐意对任何事物进行进一步阐述。
我在本例中所做的是,我已经弄清楚了用户要去的地方,并在那里进行了所有验证,否则用户无法订阅任何频道,这意味着它非常安全。