淘东电商项目(35) -SSO单点登录(登录功能完善)

引言

本文代码已提交至Github(版本号:725238a1d0c829ee28cdef0ffe49e5f1c0020a2b),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

阅读本文时,建议先阅读前面博客:

前面的文章已经把XXL-SSO集成到我们的「淘东电商」项目了,而且把登录界面也移植到了SSO服务,但是登录功能还是存在一些问题的,本文就来讲解一下如何完善这些问题。

本文目录结构:
l____引言
l____ 1. 存在的问题
l____ 2. SSO SessionId 的改造
l____ 3. 融合多端唯一登录
l____ 4. 效果演示
l____总结

1. 存在的问题

首先看第一个问题:在登录成功后,查看浏览器登录成功后保存的SessionId,可以看到保存到浏览器的sessionid为29_ee98bb7145634b78a9da54abf28f8a34,注意了前面的“29”为数据库的用户id,也就是说暴露了用户的id这是不安全的。

浏览器Cookie 对应数据库用户
在这里插入图片描述 在这里插入图片描述

第二个问题:我们会发现如果一个用户登录了,如果用其它端,如Android登录,会自动挤掉已经登录的一端,简单的说就是不支持多端唯一登录,但是我们希望能多端能同时登录的。

2. SSO SessionId 的改造

先看XXL-SSO生成sessionid的代码,在登录接口:
在这里插入图片描述
从上图,可以看到设置了userid为真实的用户id,然后使用帮助类来makeSessionId

解决思路,只要把上面的userid改为我们想要生成的token就可以了,修改后为如下:
在这里插入图片描述
token如何而来呢?这时候顺便吧多端唯一登录顺便讲解了,请继续往下看。

3. 融合唯一登录

在前面的的博客里 《淘东电商项目(20) -会员唯一登录》,我们实现了多端的唯一登录,其唯一登录的核心代码如下:

public BaseResponse<JSONObject> login(@RequestBody UserLoginInDTO userLoginInpDTO) {
    // 1.验证参数
    String mobile = userLoginInpDTO.getMobile();
    if (StringUtils.isEmpty(mobile)) {
        return setResultError("手机号码不能为空!");
    }
    String password = userLoginInpDTO.getPassword();
    if (StringUtils.isEmpty(password)) {
        return setResultError("密码不能为空!");
    }
    // 判断登陆类型
    String loginType = userLoginInpDTO.getLoginType();
    if (StringUtils.isEmpty(loginType)) {
        return setResultError("登陆类型不能为空!");
    }
    // 目的是限制范围
    if (!(loginType.equals(Constants.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_IOS)
            || loginType.equals(Constants.MEMBER_LOGIN_TYPE_PC))) {
        return setResultError("登陆类型出现错误!");
    }

    // 设备信息
    String deviceInfor = userLoginInpDTO.getDeviceInfor();
    if (StringUtils.isEmpty(deviceInfor)) {
        return setResultError("设备信息不能为空!");
    }

    // 2.对登陆密码实现加密
    String newPassWord = MD5Util.MD5(password);
    // 3.使用手机号码+密码查询数据库 ,判断用户是否存在
    UserDo userDo = userMapper.login(mobile, newPassWord);
    if (userDo == null) {
        return setResultError("用户名称或者密码错误!");
    }
    TransactionStatus transactionStatus = null;
    try {

        // 1.获取用户UserId
        Long userId = userDo.getUserId();
        // 2.生成用户令牌Key
        String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType;

        // 5.根据userId+loginType 查询当前登陆类型账号之前是否有登陆过,如果登陆过 清除之前redistoken


        UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
        transactionStatus = manualTransaction.begin();
        // // ####开启手动事务
        if (userTokenDo != null) {
            // 如果登陆过 清除之前redistoken
            String oriToken = userTokenDo.getToken();
            // 移除Token
            generateToken.removeToken(oriToken);
            int updateTokenAvailability = userTokenMapper.updateTokenAvailability(oriToken);
            if (updateTokenAvailability < 0) {
                manualTransaction.rollback(transactionStatus);
                return setResultError("系统错误");
            }
        }

        // 4.将用户生成的令牌插入到Token记录表中
        UserTokenDo userToken = new UserTokenDo();
        userToken.setUserId(userId);
        userToken.setLoginType(userLoginInpDTO.getLoginType());
        String newToken = generateToken.createToken(keyPrefix, userId + "");
        userToken.setToken(newToken);
        userToken.setDeviceInfor(deviceInfor);
        int result = userTokenMapper.insertUserToken(userToken);
        if (!toDaoResult(result)) {
            manualTransaction.rollback(transactionStatus);
            return setResultError("系统错误!");
        }

        // #######提交事务
        JSONObject data = new JSONObject();
        data.put("token", newToken);
        manualTransaction.commit(transactionStatus);
        return setResultSuccess(data);
    } catch (Exception e) {
        try {
            // 回滚事务
            manualTransaction.rollback(transactionStatus);
        } catch (Exception e1) {
        }
        return setResultError("系统错误!");
    }
}

代码的核心思想是通过会员的token表去控制,保证多个端(Android、IOS、H5等)登录成功,token表只能允许一个用户一个端登录只能有一条数据有效。

同时里面的代码token不仅放到了数据库,还托管到了Redis服务器,因为XXL-SSO框架已经帮我们做好了Redis部分,所以这里的Redis不用处理了。最后改写的代码如下(注意加了事务):

扫描二维码关注公众号,回复: 9983346 查看本文章
@Transactional
public BaseResponse<UserLoginInOutDTO> ssoLogin(@RequestBody UserLoginInDTO userLoginInpDTO) {
    // 1.验证参数
    String mobile = userLoginInpDTO.getMobile();
    if (StringUtils.isEmpty(mobile)) {
        return setSSOResultError("手机号码不能为空!");
    }
    String password = userLoginInpDTO.getPassword();
    if (StringUtils.isEmpty(password)) {
        return setSSOResultError("密码不能为空!");
    }
    // 判断登陆类型
    String loginType = userLoginInpDTO.getLoginType();
    if (StringUtils.isEmpty(loginType)) {
        return setSSOResultError("登陆类型不能为空!");
    }
    // 目的是限制范围
    if (!(loginType.equals(Constants.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_IOS)
            || loginType.equals(Constants.MEMBER_LOGIN_TYPE_PC))) {
        return setSSOResultError("登陆类型出现错误!");
    }

    // 设备信息
    String deviceInfor = userLoginInpDTO.getDeviceInfor();
    if (StringUtils.isEmpty(deviceInfor)) {
        return setSSOResultError("设备信息不能为空!");
    }
    // 2.对登陆密码实现加密
    String newPassWord = MD5Util.MD5(password);
    // 3.使用手机号码+密码查询数据库 ,判断用户是否存在
    UserDo userDo = userMapper.login(mobile, newPassWord);
    if (userDo == null) {
        return setSSOResultError("用户名称或者密码错误!");
    }

    //token操作

    Long userId = userDo.getUserId();
    // 2.生成用户令牌Key
    String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType;
    UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
    if (userTokenDo != null) {
        // 如果登陆过 清除之前redistoken
        String oriToken = userTokenDo.getToken();
        userTokenMapper.updateTokenAvailability(oriToken);
    }

    // 4.将用户生成的令牌插入到Token记录表中
    UserTokenDo userToken = new UserTokenDo();
    userToken.setUserId(userId);
    userToken.setLoginType(userLoginInpDTO.getLoginType());
    String newToken = keyPrefix + UUID.randomUUID().toString().replace("-", "");
    userToken.setToken(newToken);
    userToken.setDeviceInfor(deviceInfor);
    userTokenMapper.insertUserToken(userToken);

    userDo.setToken(newToken);

    return setSSOResultSuccess(BeanUtils.doToDto(userDo, UserLoginInOutDTO.class));
}

看倒数第二行代码,把生成的token设置进了userDo,顺便解决了第一个问题里token获取的问题了。

4.效果演示

登录成功后,可以看到浏览器Cookie和Redis不再保存用户的id了:

Cookie Redis
在这里插入图片描述 在这里插入图片描述

同时token表里,如下:
在这里插入图片描述

总结

本文主要讲解登录功能的完善,完善了安全性和多端登录唯一性的问题。

发布了2681 篇原创文章 · 获赞 5108 · 访问量 50万+

猜你喜欢

转载自blog.csdn.net/qq_20042935/article/details/104960448