SpringBoot集成WebSocket实现单用户登录(强制下线)
一、Java后端
引入WebSocket依赖
<!-- SpringWebSocket依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket类:用户登录逻辑处理,通过loginType(1:单用户登录,2:多用户登录)判断该账户是单用户登录还是多用户登录。
单用户:将userId和session键值对存入userInfoMap ,账户每次登录时判断是否已经存在userId,如果有则重置websocket,前端收到断开信息执行断开调用(清除前端token和vuex数据等);
多用户:每次登录都将token和session键值对存入userCountMap,如果存在用户大于1则发送异地登录短信。
@Component
@Slf4j
@ServerEndpoint("/webSocket/{token}/{loginType}")
public class WebSocket {
// 单用户(用户登录标识)
private static ConcurrentHashMap<String, Session> userInfoMap = new ConcurrentHashMap<>();
// 单个账户用户在线个数(用户多地登录判断)
private static ConcurrentHashMap<String, Integer> userCountMap = new ConcurrentHashMap<>();
// 是否已经发过短信
private static ConcurrentHashMap<String, Boolean> sendEmailFlagMap = new ConcurrentHashMap<>();
// 在线人数
private static int onlineCount = 0;
private static SysUserService sysUserService;
@Autowired
public void setChatService(SysUserService sysUserService) {
WebSocket.sysUserService = sysUserService;
}
/**
* @description websocket发送消息
* @param token
* @param message
*/
@OnMessage
public void onMessage(@PathParam("token") String token, String message) {
log.info(token);
String userId = JwtUtil.getUserIdByJwtToken(token);
try {
// 向指定用户发送消息
userInfoMap.get(userId).getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 建立连接调用的方法
* @param session
* @param token
* @param loginType (1:单用户登录,2:多用户登录)
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token, @PathParam("loginType") int loginType) throws IOException {
String userId = JwtUtil.getUserIdByJwtToken(token);
// 单地登录、如果存在socket则强制下线
if (userInfoMap.containsKey(userId) && loginType == 1) {
// 关闭socket(强制下线)
userInfoMap.get(userId).close();
userInfoMap.remove(userId);
// 重新开启socket(重置socket)
} else if (loginType == 2) {
// 保存session
userInfoMap.put(token, session);
// 单账户同一在线用户计数
userCountMap.put(userId, userCountMap.getOrDefault(userId, 0) + 1);
onlineCount = userInfoMap.size();
log.info("建立连接成功,当前人数为" + onlineCount);
// 如果同一在线用户大于1且没发过邮箱则发送邮箱
if (userCountMap.get(userId) > 1 && sendEmailFlagMap.getOrDefault(token, true)) {
sendEmailFlagMap.put(token, false);
String email = sysUserService.getById(userId).getEmail();
EmailUtil.sendEmail(email, "账号管理系统-异地登录提醒", "你的账号于" + LocalDateTime.now() + "在异地登录。");
}
return;
}
userInfoMap.put(userId, session);
onlineCount = userInfoMap.size();
log.info("建立连接成功,当前人数为" + onlineCount);
}
/**
* 关闭链接调用接口
* @param token
* @param loginType (1:单用户登录,2:多用户登录)
*/
@OnClose
public void onClose(@PathParam("token") String token, @PathParam("loginType") int loginType) {
log.info(token);
String userId = JwtUtil.getUserIdByJwtToken(token);
if (loginType == 1) {
userInfoMap.remove(userId);
onlineCount = userInfoMap.size();
log.info("断开连接成功,当前人数为" + onlineCount);
} else if (loginType == 2) {
userInfoMap.remove(token);
sendEmailFlagMap.remove(token);
userCountMap.put(userId, userCountMap.getOrDefault(userId, 0) - 1);
onlineCount = userInfoMap.size();
log.info(userCountMap.toString());
}
}
}
二、Vue前端
前端调用initWebSocket方法获取websocket对象,将websocket对象存在Vuex。
import { getToken, removeToken } from '@/utils/auth'
import { MessageBox } from 'element-ui'
import router, { resetRouter } from '@/router'
import store from '@/store'
export default {
initWebSocket() {
console.log('initWebSocket方法')
// 根据自己的方法获取userCode
const token = getToken()
// WebSocket地址为接口地址,http用ws、https用wws
// var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws"
const websocket = new WebSocket(`ws://localhost:8080/webSocket/${token}/${store.getters.loginType}`)
websocket.onopen = this.webSocketOpen
websocket.onerror = this.webSocketError
websocket.onmessage = this.webSocketMessage
websocket.onclose = this.webSocketClose
return websocket
},
// 连接时调用
webSocketOpen() {
console.log('WebSocket连接成功')
},
// 错误时调用
webSocketError() {
console.log('WebSocket连接错误')
},
// 接收服务器返回的数据
webSocketMessage(e) {
console.log('服务器返回的消息:', e.data)
},
// 断开时调用
webSocketClose(e) {
console.log('WebSocket连接断开', e)
removeToken()
MessageBox.confirm('您的账户在别处登录, 您被强制下线!', '异地登录提醒', {
closeOnClickModal: false,
confirmButtonText: '确定',
type: 'warning',
showCancelButton: false
}).then(() => {
resetRouter()
store.commit('RESET_STATE')
router.push('/login')
}).catch(() => {
})
}
}
三、Token工具类
public class JwtUtil {
public static final long EXPIRE = 1000 * 60 * 60 * 24; //存在时间
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //密钥
public static String getJwtToken(String id, String username) {
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("ams-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("username", username)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) {
return false;
}
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取用户id
* @param jwtToken
* @return
*/
public static String getUserIdByJwtToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) {
return "";
}
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("id");
}
/**
* 根据token获取用户账户
* @param jwtToken
* @return
*/
public static String getUsernameByJwtToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) {
return "";
}
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
}
}
四、总结
通过websocket保持前后端通信,一个websocket连接就是一个账户用户的登录。后端处理相应逻辑。