Netty分布式聊天室 - 登录功能实现

Message与LoginMessage改造

        Message:


/**
 * @author 14501
 */
@Data
public abstract class Message implements Serializable {

    /**
     * 根据消息类型字节,获得对应的消息 class
     * @param messageType 消息类型字节
     * @return 消息 class
     */
    public static Class<? extends Message> getMessageClass(int messageType) {
        return messageClasses.get(messageType);
    }

    private Long userId;

    private int messageType;

    private String token;

    private Long id;

    public abstract int getMessageType();

    public static final int LoginRequestMessage = 0;
    public static final int LoginResponseMessage = 1;
    public static final int ChatRequestMessage = 2;
    public static final int ChatResponseMessage = 3;
    public static final int GroupCreateRequestMessage = 4;
    public static final int GroupCreateResponseMessage = 5;
    public static final int GroupJoinRequestMessage = 6;
    public static final int GroupJoinResponseMessage = 7;
    public static final int GroupQuitRequestMessage = 8;
    public static final int GroupQuitResponseMessage = 9;
    public static final int GroupChatRequestMessage = 10;
    public static final int GroupChatResponseMessage = 11;
    public static final int GroupMembersRequestMessage = 12;
    public static final int GroupMembersResponseMessage = 13;
    public static final int PingMessage = 14;
    public static final int PongMessage = 15;
    /**
     * 请求类型 byte 值
     */
    public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
    /**
     * 响应类型 byte 值
     */
    public static final int  RPC_MESSAGE_TYPE_RESPONSE = 102;

    private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();

    static {
        messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
        messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
        messageClasses.put(ChatRequestMessage, com.dftdla.server.message.request.ChatRequestMessage.class);
        messageClasses.put(ChatResponseMessage, com.dftdla.server.message.response.ChatResponseMessage.class);
        messageClasses.put(GroupCreateRequestMessage, com.dftdla.server.message.request.GroupCreateRequestMessage.class);
        messageClasses.put(GroupCreateResponseMessage, com.dftdla.server.message.response.GroupCreateResponseMessage.class);
        messageClasses.put(GroupJoinRequestMessage, com.dftdla.server.message.request.GroupJoinRequestMessage.class);
        messageClasses.put(GroupJoinResponseMessage, com.dftdla.server.message.response.GroupJoinResponseMessage.class);
        messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
        messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
        messageClasses.put(GroupChatRequestMessage, com.dftdla.server.message.request.GroupChatRequestMessage.class);
        messageClasses.put(GroupChatResponseMessage, com.dftdla.server.message.response.GroupChatResponseMessage.class);
        messageClasses.put(GroupMembersRequestMessage, com.dftdla.server.message.request.GroupMembersRequestMessage.class);
        messageClasses.put(GroupMembersResponseMessage, com.dftdla.server.message.response.GroupMembersResponseMessage.class);
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }

}

        LoginRequestMessage:

/**
 * @author 14501
 */
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class LoginRequestMessage extends Message {

    public LoginRequestMessage() {
    }

    @Override
    public int getMessageType() {
        return LoginRequestMessage;
    }
}

WebSocketHandler和QuitHandler改造方案

        WebSocketHandler我们用于对WS数据的解析和鉴权

        QuitHandler我们用于对异常和正常关闭的channel的处理

WebSocketHandler改造

        解析接收到的TextWebSocketFrame数据,并对解析结果进行鉴权检查

        解析工具类,使用FastJson2:

/**
 * @author 14501
 */
public class MessageUtil {
    public static Message decode(String msg) {
        Message message = null;
        try {
            JSONObject temp = JSONObject.parse(msg);
            message = JSONObject.parseObject(msg, Message.getMessageClass((Integer) temp.get("messageType")));
        }catch (Exception e) {
            e.printStackTrace();
        }
        return message;
    }
}

        WebSocketHandler:


/**
 * @author 14501
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Resource
    private ChatUserService chatUserService;

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        Message decode = MessageUtil.decode(textWebSocketFrame.text());
        log.info("解析指令:{}",decode);
        boolean login = decode.getMessageType() == Message.LoginRequestMessage;

        if(login || doAuth(decode.getToken())){
            //如果不是登录指令,则说明token鉴权通过,我们设置一个userId给Message以替代ThreadLocal保存的UserId,这样不用再手动回收
            if(!login){
                decode.setUserId(Long.valueOf(JwtUtil.parseJWT(decode.getToken()).getSubject()));
            }
            //交给下面的Handler进行类型匹配
            channelHandlerContext.fireChannelRead(decode);
        }else{
            //鉴权未通过,关闭channel
            channelHandlerContext.writeAndFlush(new TextWebSocketFrame(new ResponseResult<String>(Code.USER_AUTH_ERROR).toString()));
            log.warn("非法的token!");
            ChannelUtils.close(channelHandlerContext.channel());
        }
    }

    /**
     * 鉴定token是否合法
     * @param token 待校验Auth
     * @return true合法
     */
    public boolean doAuth(String token){
        return chatUserService.doAuth(token);
    }
}

        chatUserService鉴权方案:

    @Override
    public boolean doAuth(String token) {
        boolean res;
        Claims claims;

        try {
            claims = JwtUtil.parseJWT(token);
        }catch (Exception e) {
            e.printStackTrace();
            claims = null;
        }

        if(claims == null){
            //鉴权失败
            res = false;
        }else{
            //token合法,检查是否是最新的token
            Long userId = Long.valueOf(claims.getSubject());
            Object object = redisCache.getCacheObject(RedisKey.loginKey(userId));
            res = object != null && object.equals(token);
        }
        return res;
    }

        如此一来,后续handler接收到的请求都是合法请求,不需要验证权限,但指令数据是否合法还需判断,如果不嫌麻烦可以对Message添加一个自检方法,在WebSocketHandler这里运行一次自检也可以,我们后续默认指令数据合法

QuitHandler改造        

        对QuitHandler的改造实际上是Session的unbind方法的改造,在bind时会记录用户登录操作并在redis存储token,在unbind时会记录用户登出操作,并移除redis中的token

        QuitHandler:

/**
 * @author 14501
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class QuitHandler extends ChannelInboundHandlerAdapter {
    @Resource
    private Session session;

    /**
     * 当连接断开时触发 inactive 事件
     * @param ctx channel
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        session.unbind(ctx.channel());
        log.debug("{} 已经断开", ctx.channel());
    }

    /**
     * 当出现异常时触发
     * @param ctx channel
     * @param cause 原因
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        session.unbind(ctx.channel());
        log.debug("{} 已经异常断开 异常是{}", ctx.channel(), cause.getMessage());
    }
}

        chatUserService的login和logout:


    @Override
    public String login(Long userId) {
        String token = JwtUtil.createJWT(userId + "");
        redisCache.setCacheObject(RedisKey.loginKey(userId),token,12, TimeUnit.HOURS);

        //登录记录
        UserHandler userHandler = new UserHandler();
        userHandler.setUserId(userId);
        userHandler.setType(UserHandler.LOGIN);
        userHandlerService.save(userHandler);

        return token;
    }

    @Override
    public void logout(Long userId) {
        redisCache.deleteObject(RedisKey.loginKey(userId));

        UserHandler userHandler = new UserHandler();
        userHandler.setUserId(userId);
        userHandler.setType(UserHandler.LOGOUT);
        userHandlerService.save(userHandler);
    }

        如此QuitHandler的全局Channel关闭也完成了

登录功能实现

        LoginMessageHandler,后面当我们登录时需要拉取所有离线消息:


/**
 * @author 14501
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {

    @Resource
    private ChatUserService chatUserService;
    @Resource
    private Session session;
    @Resource
    private RedisCache redisCache;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
        Long userId = msg.getUserId();
        boolean login = chatUserService.canLogin(userId);

        LoginResponseMessage message;

        if (login) {
            String bind = session.bind(ctx.channel(), userId);
            log.info("登陆成功");
            message = new LoginResponseMessage(true, "登录成功");
            message.setToken(bind);
        } else {
            log.warn("用户不合法或已登录");
            message = new LoginResponseMessage(false, "用户不合法或已登录");
        }

        ctx.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS.code,message).toString()));
    }
}

        这里有一个登录时拉取消息的问题:对于离线消息,我们存储在Redis中,如果Redis执行淘汰策略,把数据清空了,我们该如何判断离线消息?

        这里我的思路是:在数据发送时存储数据,缓存离线消息,如果离线消息被删除,则用户通过漫游查询消息(类似微信太久没登陆会导致消息丢失)

        另一种方式是存储消息的表添加一个消息状态,我们这里只是作为一个示例项目,就采用上面的方式了

注意事项

        对于WS连接,我们在write数据时,要使用TextWebSocketFrame进行包装,否则客户端接收不到消息

        并且Message的子类中,以往的代指username的Stirng类型数据都要改为Long类型(我们用于代指UserId)

调试

        使用ApiFox的WebSocket接口测试,注意这里每开一个窗口,服务器那边都对应唯一的channe,即:一个窗口就是一个ws客户端

         服务器日志:

        下一篇:单聊功能实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱飞的男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值