使用WebSocket实现。
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig配置类如下:
@Configuration
@EnableWebSocketMessageBroker //注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
//注册STOMP协议的节点(endpoint),并映射指定的url,这样前端可以通过websocket连接上服务
//也就是我们配置websocket的服务地址,并且支持SocketJS
public void registerStompEndpoints(StompEndpointRegistry registry) {
//将/ws/ep路径注册为节点,用户连接了这个节点就可以进行websocket通讯
//允许跨域
//支持SocketJS访问
registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();
}
@Override
//输入通道参数配置(websocket连接时携带了token令牌,因此需要这个配置)
//registration注册 interceptors拦截器 channel通道
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//判断是否为连接,如果是,需要获取token,并且设置用户对象
if (StompCommand.CONNECT.equals(accessor.getCommand())){
//Auth-Token是前端传来的
String token = accessor.getFirstNativeHeader("Auth-Token");
if (!StringUtils.isEmpty(token)){
String authToken = token.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
if (!StringUtils.isEmpty(username)){
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//验证token是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)){
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
accessor.setUser(authenticationToken);
}
}
}
}
return message;
}
});
}
@Override
//配置消息代理(Message Broker)
public void configureMessageBroker(MessageBrokerRegistry registry) {
//配置代理域,可以配置多个,配置代理目的地前缀为/queue,可以在配置域上向客户端推送消息
registry.enableSimpleBroker("/queue");
}
}
注意,/ws/ep是连接节点,/queue是消息的“中转站”,消息都发送到/queue。
配置完后,开始写业务代码。先看相关的pojo类:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ChatMsg {
private String from;
private String to;
private String content;
private LocalDateTime date;
private String formNickName;
}
在线聊天的流程为:
前端使用SockJS连接websocket,节点是/ws/ep,连接时携带了一个token令牌(Auth-Token),然后就可以向/queue发送消息了。但是注意,该项目前端发送消息时,是先发送到后端接口(/ws/chat)的,对应的controller是WsController,如下:
@Controller
public class WsController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/ws/chat")
public void handleMsg(Authentication authentication, ChatMsg chatMsg){
Admin admin = (Admin) authentication.getPrincipal();
chatMsg.setFrom(admin.getUsername());
chatMsg.setFormNickName(admin.getName());
chatMsg.setDate(LocalDateTime.now());
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg);
}
}
注意,这里不能用@RestController,而是用@Controller。后端把该消息发送到了/queue/chat,之后另一个前端会subscribe订阅/queue/chat里的消息,从而实现两个前端间的消息互传,实现在线聊天。