SpringBoot+Shiro学习之密码加密和登录失败次数限制示例
这个项目写到现在,基本的雏形出来了,在此感谢一直关注的童鞋,送你们一句最近刚学习的一句鸡汤:念念不忘,必有回响。再贴一张ui图片:
前篇思考问题解决
前篇我们只是完成了同一账户的登录人数限制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; } }
代码中注释比较完善,也可以去下载源码查看,这样结合看,跟容易理解,不懂的在评论区留言,看见必回!
对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这个拦截器里面做的修改。
目标:
- 现在项目里面的密码整个流程都是以明文的方式传递的。这样在实际应用中是很不安全的,京东,开源中国等这些大公司都有泄库事件,这样对用户的隐私造成巨大的影响,所以将密码加密存储传输就非常必要了。
- 密码重试次数限制,也是出于安全性的考虑。
实现目标一:
shiro本身是有对密码加密进行实现的,提供了passwordservice及credentialsmatcher用于提供加密密码及验证密码服务。
我就是自己实现的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()); }
demo下载地址:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: Java 值传递和引用传递详解及实例代码