SpringBoot+Shiro学习之密码加密和登录失败次数限制

这个项目写到现在,基本的雏形出来了,在此感谢一直关注的童鞋,送你们一句最近刚学习的一句鸡汤:念念不忘,必有回响。再贴一张ui图片:

z77z后台管理系统

·······················································································································································

个人博客:http://z77z.oschina.io/

此项目下载地址:https://git.oschina.net/z77z/springboot_mybatisplus

·······················································································································································

前篇思考问题解决


  1. 前篇我们只是完成了同一账户的登录人数限制shiro拦截器的编写,对于手动踢出用户的功能只是说了采用在session域中添加一个key为kickout的布尔值,由之前编写的KickoutSessionControlFilter拦截器来判断是否将用户踢出,还没有说怎么获取当前在线用户的列表的核心代码,下面贴出来:
/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author z77z
 * @since 2017-02-10
 */
@Service
public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> {
    @Autowired
    RedisSessionDAO redisSessionDAO;

    public Page<UserOnlineBo> getPagePlus(FrontPage<UserOnlineBo> frontPage) {
        // 因为我们是用redis实现了shiro的session的Dao,而且是采用了shiro+redis这个插件
        // 所以从spring容器中获取redisSessionDAO
        // 来获取session列表.
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        Iterator<Session> it = sessions.iterator();
        List<UserOnlineBo> onlineUserList = new ArrayList<UserOnlineBo>();
        Page<UserOnlineBo> pageList = frontPage.getPagePlus();
        // 遍历session
        while (it.hasNext()) {
            // 这是shiro已经存入session的
            // 现在直接取就是了
            Session session = it.next();
            // 如果被标记为踢出就不显示
            Object obj = session.getAttribute("kickout");
            if (obj != null)
                continue;
            UserOnlineBo onlineUser = getSessionBo(session);
            onlineUserList.add(onlineUser);
        }
        // 再将List<UserOnlineBo>转换成mybatisPlus封装的page对象
        int page = frontPage.getPage() - 1;
        int rows = frontPage.getRows() - 1;
        int startIndex = page * rows;
        int endIndex = (page * rows) + rows;
        int size = onlineUserList.size();
        if (endIndex > size) {
            endIndex = size;
        }
        pageList.setRecords(onlineUserList.subList(startIndex, endIndex));
        pageList.setTotal(size);
        return pageList;
    }
    //从session中获取UserOnline对象
    private UserOnlineBo getSessionBo(Session session){
        //获取session登录信息。
        Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
        if(null == obj){
            return null;
        }
        //确保是 SimplePrincipalCollection对象。
        if(obj instanceof SimplePrincipalCollection){
            SimplePrincipalCollection spc = (SimplePrincipalCollection)obj;
            /**
             * 获取用户登录的,@link SampleRealm.doGetAuthenticationInfo(...)方法中
             * return new SimpleAuthenticationInfo(user,user.getPswd(), getName());的user 对象。
             */
            obj = spc.getPrimaryPrincipal();
            if(null != obj && obj instanceof SysUser){
                //存储session + user 综合信息
                UserOnlineBo userBo = new UserOnlineBo((SysUser)obj);
                //最后一次和系统交互的时间
                userBo.setLastAccess(session.getLastAccessTime());
                //主机的ip地址
                userBo.setHost(session.getHost());
                //session ID
                userBo.setSessionId(session.getId().toString());
                //session最后一次与系统交互的时间
                userBo.setLastLoginTime(session.getLastAccessTime());
                //回话到期 ttl(ms)
                userBo.setTimeout(session.getTimeout());
                //session创建时间
                userBo.setStartTime(session.getStartTimestamp());
                //是否踢出
                userBo.setSessionStatus(false);
                return userBo;
            }
        }
        return null;
    }
}

代码中注释比较完善,也可以去下载源码查看,这样结合看,跟容易理解,不懂的在评论区留言,看见必回!

  1. 对Ajax请求的优化:这里有一个前提,我们知道Ajax不能做页面redirect和forward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出了。也就是说在KickoutSessionControlFilter拦截器拦截后,正常如果被踢出,就会跳转到被踢出的提示页面,如果是Ajax请求,给用户的感觉就是没有感觉,核心解决代码如下:
Map<String, String> resultMap = new HashMap<String, String>();
//判断是不是Ajax请求
if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
    resultMap.put("user_status", "300");
    resultMap.put("message", "您已经在其他地方登录,请重新登录!");
    //输出json串
    out(response, resultMap);
}else{
    //重定向
    WebUtils.issueRedirect(request, response, kickoutUrl);
}

private void out(ServletResponse hresponse, Map<String, String> resultMap)
    throws IOException {
    try {
        hresponse.setCharacterEncoding("UTF-8");
        PrintWriter out = hresponse.getWriter();
        out.println(JSON.toJSONString(resultMap));
        out.flush();
        out.close();
    } catch (Exception e) {
        System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
    }
}

这是在KickoutSessionControlFilter这个拦截器里面做的修改。

目标:


  1. 现在项目里面的密码整个流程都是以明文的方式传递的。这样在实际应用中是很不安全的,京东,开源中国等这些大公司都有泄库事件,这样对用户的隐私造成巨大的影响,所以将密码加密存储传输就非常必要了。

  2. 密码重试次数限制,也是出于安全性的考虑。

实现目标一:


shiro本身是有对密码加密进行实现的,提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务。这里我觉得这种过于麻烦,大家有需要可以去看这篇博客:shiro加密解密

我就是自己实现的EDS加密,并且保存的加密明文是采用password+username的方式,减小了密码相同,密文也相同的问题,这里我只是贴一下,EDS的加密解密代码,另外我还改了MyShiroRealm文件,再查数据库的时候加密后再查,而且在创建用户的时候不要忘记的加密存到数据库。这里就补贴代码了。

/**
 * DES加密解密
 * 
 * @author z77z
 * @datetime 2017-3-13
 */
public class MyDES {
    /**
     * DES算法密钥
     */
    private static final byte[] DES_KEY = { 21, 1, -110, 82, -32, -85, -128, -65 };

    /**
     * 数据加密,算法(DES)
     * 
     * @param data
     *            要进行加密的数据
     * @return 加密后的数据
     */
    @SuppressWarnings("restriction")
    public static String encryptBasedDes(String data) {
        String encryptedData = null;
        try {
            // DES算法要求有一个可信任的随机数源
            SecureRandom sr = new SecureRandom();
            DESKeySpec deskey = new DESKeySpec(DES_KEY);
            // 创建一个密匙工厂,然后用它把DESKeySpec转换成一个SecretKey对象
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
            SecretKey key = keyFactory.generateSecret(deskey);
            // 加密对象
            Cipher cipher = Cipher.getInstance("DES");
            cipher.init(Cipher.ENCRYPT_MODE, key, sr);
            // 加密,并把字节数组编码成字符串
            encryptedData = new sun.misc.BASE64Encoder().encode(cipher.doFinal(data.getBytes()));
        } catch (Exception e) {
            // log.error("加密错误,错误信息:", e);
            throw new RuntimeException("加密错误,错误信息:", e);
        }
        return encryptedData;
    }

    /**
     * 数据解密,算法(DES)
     * 
     * @param cryptData
     *            加密数据
     * @return 解密后的数据
     */
    @SuppressWarnings("restriction")
    public static String decryptBasedDes(String cryptData) {
        String decryptedData = null;
        try {
            // DES算法要求有一个可信任的随机数源
            SecureRandom sr = new SecureRandom();
            DESKeySpec deskey = new DESKeySpec(DES_KEY);
            // 创建一个密匙工厂,然后用它把DESKeySpec转换成一个SecretKey对象
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
            SecretKey key = keyFactory.generateSecret(deskey);
            // 解密对象
            Cipher cipher = Cipher.getInstance("DES");
            cipher.init(Cipher.DECRYPT_MODE, key, sr);
            // 把字符串解码为字节数组,并解密
            decryptedData = new String(cipher.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(cryptData)));
        } catch (Exception e) {
            // log.error("解密错误,错误信息:", e);
            throw new RuntimeException("解密错误,错误信息:", e);
        }
        return decryptedData;
    }

    public static void main(String[] args) {
        String str = "123456";
        // DES数据加密
        String s1 = encryptBasedDes(str);
        System.out.println(s1);
        // DES数据解密
        String s2 = decryptBasedDes(s1);
        System.err.println(s2);
    }
}

实现目标二


如在1个小时内密码最多重试5次,如果尝试次数超过5次就锁定1小时,1小时后可再次重试,如果还是重试失败,可以锁定如1天,以此类推,防止密码被暴力破解。我们使用redis数据库来保存当前用户登录次数,也就是执行身份认证方法:MyShiroRealm.doGetAuthenticationInfo()的次数,如果登录成功就清空计数。超过就返回相应错误信息。(redis的具体操作可以去看我之前的springboot+redis的一篇博客)根据这个逻辑,修改MyShiroRealm.java如下:

/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
 * 
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken authcToken) throws AuthenticationException {
    
    
    System.out.println("身份认证方法:MyShiroRealm.doGetAuthenticationInfo()");
    
    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    String name = token.getUsername();
    String password = String.valueOf(token.getPassword());
    //访问一次,计数一次
    ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
    opsForValue.increment(SHIRO_LOGIN_COUNT+name, 1);
    //计数大于5时,设置用户被锁定一小时
    if(Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT+name))>=5){
        opsForValue.set(SHIRO_IS_LOCK+name, "LOCK");
        stringRedisTemplate.expire(SHIRO_IS_LOCK+name, 1, TimeUnit.HOURS);
    }
    if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK+name))){
        throw new DisabledAccountException("由于密码输入错误次数大于5次,帐号已经禁止登录!");
    }
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("nickname", name);
    //密码进行加密处理  明文为  password+name
    String paw = password+name;
    String pawDES = MyDES.encryptBasedDes(paw);
    map.put("pswd", pawDES);
    SysUser user = null;
    // 从数据库获取对应用户名密码的用户
    List<SysUser> userList = sysUserService.selectByMap(map);
    if(userList.size()!=0){
        user = userList.get(0);
    } 
    if (null == user) {
        throw new AccountException("帐号或密码不正确!");
    }else if(user.getStatus()==0){
        /**
         * 如果用户的status为禁用。那么就抛出<code>DisabledAccountException</code>
         */
        throw new DisabledAccountException("此帐号已经设置为禁止登录!");
    }else{
        //登录成功
        //更新登录时间 last login time
        user.setLastLoginTime(new Date());
        sysUserService.updateById(user);
        //清空登录计数
        opsForValue.set(SHIRO_LOGIN_COUNT+name, "0");
    }
    return new SimpleAuthenticationInfo(user, password, getName());
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容