微信公眾號:一個優秀的廢人。如有問題,請後臺留言,反正我也不會聽。
前言
昨天那篇介紹了 WebSocket 實現廣播,也即伺服器端有訊息時,將訊息傳送給所有連線了當前 endpoint 的瀏覽器。但這無法解決訊息由誰傳送,又由誰接收的問題。所以,今天寫一篇實現一對一的聊天室。
今天這一篇建立在昨天那一篇的基礎之上,為便於更好理解今天這一篇,推薦先閱讀:「SpringBoot 整合WebSocket 實現廣播訊息 」
準備工作
- Spring Boot 2.1.3 RELEASE
- Spring Security 2.1.3 RELEASE
- IDEA
- JDK8
pom 依賴
因聊天室涉及到使用者相關,所以在上一篇基礎上引入 Spring Security 2.1.3 RELEASE 依賴
<!-- Spring Security 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
複製程式碼
Spring Security 的配置
雖說涉及到 Spring Security ,但鑑於篇幅有限,這裡只對這個專案相關的部分進行介紹,具體的 Spring Security 教程,後面會出。
這裡的 Spring Security 配置很簡單,具體就是設定登入路徑、設定安全資源以及在記憶體中建立使用者和密碼,密碼需要注意加密,這裡使用 BCrypt 加密演算法在使用者登入時對密碼進行加密。 程式碼註釋很詳細,不多說。
package com.nasus.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
// 開啟Spring Security的功能
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 設定 SpringSecurity 對 / 和 "/login" 路徑不攔截
.mvcMatchers("/","/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 設定 Spring Security 的登入頁面訪問路徑為/login
.loginPage("/login")
// 登入成功後轉向 /chat 路徑
.defaultSuccessUrl("/chat")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 在記憶體中分配兩個使用者 nasus 和 chenzy ,使用者名稱和密碼一致
// BCryptPasswordEncoder() 是 Spring security 5.0 中新增的加密方式
// 登陸時用 BCrypt 加密方式對使用者密碼進行處理。
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("nasus")
// 保證使用者登入時使用 bcrypt 對密碼進行處理再與記憶體中的密碼比對
.password(new BCryptPasswordEncoder().encode("nasus")).roles("USER")
.and()
// 登陸時用 BCrypt 加密方式對使用者密碼進行處理。
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("chenzy")
// 保證使用者登入時使用 bcrypt 對密碼進行處理再與記憶體中的密碼比對
.password(new BCryptPasswordEncoder().encode("chenzy")).roles("USER");
}
@Override
public void configure(WebSecurity web) throws Exception {
// /resource/static 目錄下的靜態資源,Spring Security 不攔截
web.ignoring().antMatchers("/resource/static**");
}
}
複製程式碼
WebSocket 的配置
在上一篇的基礎上另外註冊一個名為 "/endpointChat" 的節點,以供使用者訂閱,只有訂閱了該節點的使用者才能接收到訊息;然後,再增加一個名為 "/queue" 訊息代理。
@Configuration
// @EnableWebSocketMessageBroker 註解用於開啟使用 STOMP 協議來傳輸基於代理(MessageBroker)的訊息,這時候控制器(controller)
// 開始支援@MessageMapping,就像是使用 @requestMapping 一樣。
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//註冊一個名為 /endpointNasus 的 Stomp 節點(endpoint),並指定使用 SockJS 協議。
registry.addEndpoint("/endpointNasus").withSockJS();
//註冊一個名為 /endpointChat 的 Stomp 節點(endpoint),並指定使用 SockJS 協議。
registry.addEndpoint("/endpointChat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 廣播式配置名為 /nasus 訊息代理 , 這個訊息代理必須和 controller 中的 @SendTo 配置的地址字首一樣或者全匹配
// 點對點增加一個 /queue 訊息代理
registry.enableSimpleBroker("/queue","/nasus/getResponse");
}
}
複製程式碼
控制器 controller
指定傳送訊息的格式以及模板。詳情見,程式碼註釋。
@Autowired
//使用 SimpMessagingTemplate 向瀏覽器傳送資訊
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat")
public void handleChat(Principal principal,String msg){
// 在 SpringMVC 中,可以直接在引數中獲得 principal,principal 中包含當前使用者資訊
if (principal.getName().equals("nasus")){
// 硬編碼,如果傳送人是 nasus 則接收人是 chenzy 反之也成立。
// 通過 messageingTemplate.convertAndSendToUser 方法向使用者傳送資訊,引數一是接收訊息使用者,引數二是瀏覽器訂閱地址,引數三是訊息本身
messagingTemplate.convertAndSendToUser("chenzy",
"/queue/notifications",principal.getName()+"-send:" + msg);
} else {
messagingTemplate.convertAndSendToUser("nasus",
"/queue/notifications",principal.getName()+"-send:" + msg);
}
}
複製程式碼
登入頁面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
<title>登陸頁面</title>
</head>
<body>
<div th:if="${param.error}">
無效的賬號和密碼
</div>
<div th:if="${param.logout}">
你已登出
</div>
<form th:action="@{/login}" method="post">
<div><label> 賬號 : <input type="text" name="username"/> </label></div>
<div><label> 密碼: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登陸"/></div>
</form>
</body>
</html>
複製程式碼
聊天頁面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
<title>Home</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
聊天室
</p>
<form id="nasusForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit"/>
</form>
<script th:inline="javascript">
$('#nasusForm').submit(function(e){
e.preventDefault();
var text = $('#nasusForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
// 連線 SockJs 的 endpoint 名稱為 "/endpointChat"
var sock = new SockJS("/endpointChat");
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
// 訂閱 /user/queue/notifications 傳送的訊息,這裡與在控制器的
// messagingTemplate.convertAndSendToUser 中訂閱的地址保持一致
// 這裡多了 /user 字首,是必須的,使用了 /user 才會把訊息傳送到指定使用者
stomp.subscribe("/user/queue/notifications", handleNotification);
});
function handleNotification(message) {
$('#output').append("<b>Received: " + message.body + "</b><br/>")
}
function sendSpittle(text) {
stomp.send("/chat", {}, text);
}
$('#stop').click(function() {sock.close()});
</script>
<div id="output"></div>
</body>
</html>
複製程式碼
頁面控制器 controller
@Controller
public class ViewController {
@GetMapping("/nasus")
public String getView(){
return "nasus";
}
@GetMapping("/login")
public String getLoginView(){
return "login";
}
@GetMapping("/chat")
public String getChatView(){
return "chat";
}
}
複製程式碼
測試
預期結果應該是:兩個使用者登入系統,可以互相傳送訊息。但是同一個瀏覽器的使用者會話的 session 是共享的,這裡需要在 Chrome 瀏覽器再新增一個使用者。
具體操作在 Chrome 的 設定-->管理使用者-->新增使用者:
兩個使用者分別訪問 http://localhost:8080/login 登入系統,跳轉至聊天介面:
相互傳送訊息:
完整程式碼
https://github.com/turoDog/Demo/tree/master/springboot_websocket_demo
如果覺得對你有幫助,請給個 Star 再走唄,非常感謝。
後語
如果本文對你哪怕有一丁點幫助,請幫忙點好看。你的好看是我堅持寫作的動力。
另外,關注之後在傳送 1024 可領取免費學習資料。
資料詳情請看這篇舊文:Python、C++、Java、Linux、Go、前端、演算法資料分享