写在前面:
最近有一个想法,做一个程序员师徒体系。由于在大学期间的我在学习java的时分十分地苍茫,找不到自己的方向,也没有一个社会上有经历的前辈去辅导,所以走了许多的弯路。后来工作了,想把自己的避坑经历共享给别人,可是发现身边都是有经历的开发者,也没有机会去共享自己的想法,所以富有同学就想做一个程序员专属的师徒体系,秉承着学徒能够有人指导少走弯路,师傅能桃李满天下的意图,所以开端做这个师徒体系,也会同步更新该体系所用到的技能,并且作为教程共享给咱们,期望咱们能够关注一波。
其实谈天功用最开端的时分咱们能够创立一个表,当人们发送的时分将音讯往表里面刺进,接纳的时分将音讯从表里面取出,然后定时去取出音讯,这样勉强能实现一个音讯谈天的功用,可是会大大的耗费服务器的功用,所以咱们用到了一项新技能:WebSocket。那么老规矩,WebSocket是什么呢? 在这儿富有同学用自己的话总结一次:websocket使得服务器能够主动得向客户端推送音讯,而且服务器和客户端树立衔接只要一次承认就能够了。 好了,那么怎样将这个功用集成到师徒办理体系中来呢?在这儿富有同学要感谢wensocket开源项目,由于富有同学在改项目上进行改善使得websocket能够集成springsecurity和jwt技能到师徒办理体系中来。咱们能够先去瞧一瞧这个项目,对接下来的教程能够理解得更加深透。
废话不多说,开端咱们的实战
第一步,咱们导入相关的jar包
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<!--websocket-->
第二步,创立websocket的service类
package com.wangfugui.apprentice.service;
import com.alibaba.fastjson.JSON;
import com.wangfugui.apprentice.dao.domain.Message;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
//静态变量,用来记录当时在线衔接数。应该把它设计成线程安全的。
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,用来寄存每个客户端对应的WebSocketServer目标。
private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
//发送音讯
public void sendMessage(Session session, String message) throws IOException {
if(session != null){
synchronized (session) {
System.out.println("发送数据:" + message);
session.getBasicRemote().sendText(message);
}
}
}
//给指定用户发送信息
public void sendInfo(String userName, String message){
Session session = sessionPools.get(userName);
try {
sendMessage(session, message);
}catch (Exception e){
e.printStackTrace();
}
}
// 群发音讯
public void broadcast(String message){
for (Session session: sessionPools.values()) {
try {
sendMessage(session, message);
} catch(Exception e){
e.printStackTrace();
continue;
}
}
}
//树立衔接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "username") String userName){
sessionPools.put(userName, session);
addOnlineCount();
System.out.println(userName + "参加webSocket!当时人数为" + onlineNum);
// 广播上线音讯
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("0");
msg.setText(userName);
broadcast(JSON.toJSONString(msg,true));
}
//封闭衔接时调用
@OnClose
public void onClose(@PathParam(value = "username") String userName){
sessionPools.remove(userName);
subOnlineCount();
System.out.println(userName + "断开webSocket衔接!当时人数为" + onlineNum);
// 广播下线音讯
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("-2");
msg.setText(userName);
broadcast(JSON.toJSONString(msg,true));
}
//收到客户端信息后,根据接纳人的username把音讯推下去或许群发
// to=-1群发音讯
@OnMessage
public void onMessage(String message) throws IOException{
System.out.println("server get" + message);
Message msg=JSON.parseObject(message, Message.class);
msg.setDate(new Date());
if (msg.getTo().equals("-1")) {
broadcast(JSON.toJSONString(msg,true));
} else {
sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
}
}
//过错时调用
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("发生过错");
throwable.printStackTrace();
}
public static void addOnlineCount(){
onlineNum.incrementAndGet();
}
public static void subOnlineCount() {
onlineNum.decrementAndGet();
}
public static AtomicInteger getOnlineNumber() {
return onlineNum;
}
public static ConcurrentHashMap<String, Session> getSessionPools() {
return sessionPools;
}
}
咱们仔细看这个类里面的四个注解:
@OnOpen @OnClose @OnMessage @OnError
分别是websocket衔接,封闭,收到音讯,过错时调用 @ServerEndpoint
也要加上,接下来咱们装备一个config类使得上面的类收效:
package com.wangfugui.apprentice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebScoket装备处理器
*/
@Configuration
public class WebSocketConfig {
/**
* ServerEndpointExporter 效果
*
* 这个Bean会主动注册运用@ServerEndpoint注解声明的websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
这儿是最关键的两个装备类了。
第三步,本来到这儿就能够跑起来进行谈天了
前端的代码在库房里面,这儿就不贴出来来了。可是咱们发现由于师徒体系是集成的jwt的,所以咱们没有办法进行基本的谈天,由于都被阻拦了!所以咱们要做的第一步便是把相关接口放开:
@Override
protected void configure(HttpSecurity http) throws Exception {
// post恳求要封闭csrf验证,否则拜访报错;实践开发中敞开,需求前端配合传递其他参数
http.csrf().disable()
.authorizeRequests()
//swagger
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
//设置哪些路径不需求认证,这儿也能放行静态资源
.antMatchers("/webSocket/**").anonymous()
.antMatchers("/static/**").anonymous()
.antMatchers("/css/**", "/js/**").anonymous()
.antMatchers("/favicon.ico").anonymous()
//放开注册,登录用户接口
.antMatchers("/user/register").anonymous()
.antMatchers("/login").anonymous()
.antMatchers("/logoutSystem").anonymous()
.antMatchers("/chatroom").anonymous()
.antMatchers("/onlineusers").anonymous()
.antMatchers("/currentuser").anonymous()
.anyRequest().authenticated() // 一切恳求都需求验证
.and() //这儿选用链式编程
.logout()
//刊出成功后,调转的页面
.logoutSuccessUrl("/login")
// 装备自己的刊出URL,默以为 /logout
.logoutUrl("/logoutSystem")
// 是否销毁session,默许ture
.invalidateHttpSession(true)
// 删去指定的cookies
.deleteCookies(JwtTokenUtils.TOKEN_HEADER)
.and()
//增加用户账号的认证
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
//增加用户权限的认证
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
//咱们能够准确地操控什么机遇创立session,有以下选项进行操控:
//always – 假如session不存在总是需求创立;
//ifRequired – 仅当需求时,创立session(默许装备);
//never – 框架从不创立session,但假如已经存在,会运用该session ;
//stateless – Spring Security不会创立session,或运用session;
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
//增加没有带着token或许token无效操作
.authenticationEntryPoint(new JWTAuthenticationEntryPoint())
//增加无权限时的处理
.accessDeniedHandler(new JWTAccessDeniedHandler());
}
接着咱们更改咱们的默许登录接口:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//这儿特别注意是登录的接口,自定义登录接口,不写的话默许"/login"
super.setFilterProcessesUrl("/loginvalidate");
}
这样咱们的默许登录就能够登录了,可是由于要集成到咱们的体系中来,咱们需求jwt生成的字符串,所以在登录成功咱们要将jwt秘钥存储在cookie中来:
在JWTAuthenticationFilter
类中
// 成功验证后调用的办法
// 假如验证成功,就生成token并回来
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) {
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
boolean isRemember = rememberMe.get() == 1;
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);
// 回来创立成功的token
// 可是这儿创立的token只是单纯的token
// 依照jwt的规定,最后恳求的时分应该是 `Bearer token`
response.setHeader("Authorization", JwtTokenUtils.TOKEN_PREFIX + token);
response.addCookie(new Cookie("Authorization",token));
}
这样之后咱们在调用谈天室接口中就能够获取cookie,然后将在线列表显示出来
@RequestMapping("/chatroom")
public String chatroom(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
//假如没有cookie则回来登录页面
Cookie authCookie = Arrays.stream(cookies).filter(cookie -> cookie.getName()
.contains(JwtTokenUtils.TOKEN_HEADER)).collect(Collectors.toList()).get(0);
if (authCookie == null) {
return "login";
}
String tokenHeader = authCookie.getValue();
String username = JwtTokenUtils.getUsername(tokenHeader);
HttpSession session = request.getSession();
User idByUserName = userService.getIdByUserName(username);
session.setAttribute("uid", idByUserName.getId());
return "chatroom";
}
还有一个登录接口:
@RequestMapping("/login")
public String login(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "login";
}
//假如没有cookie则回来登录页面
List<Cookie> collect = Arrays.stream(cookies).filter(cookie -> cookie.getName()
.contains(JwtTokenUtils.TOKEN_HEADER)).collect(Collectors.toList());
if (collect.isEmpty()) {
return "login";
}
return "home";
}
咱们创立一个首页页面
<!DOCTYPE>
<html>
<head>
<title>login</title>
</head>
<body>
<div class="container vertical-center">
<a href="/chatroom">谈天室</a>
<form action="/logoutSystem" method="post">
<button type="submit" >刊出</button>
</form>
</div>
</body>
</html>
这样咱们就能够进行刊出和进入谈天室了。剩余代码就不在这儿贴出,请咱们移步到库房观看:SpringBoot+WebSocket
说在之后
师徒体系我会一直更新,由于是开源的项目,所以我也期望又更多的小伙伴参加进来!! 这是程序员师徒办理体系的地址: 程序员师徒办理体系