SpringBoot学习:整合shiro(验证码功能和登录次数限制功能)
项目下载地址:http://download.csdn.NET/detail/aqsunkai/9805821
(一)验证码
首先login.jsp里增加了获取验证码图片的标签:
<body style="margin-left: 500px"> <h1 style="margin-left: 30px">登录页面----</h1> <form action="<%=basePath%>/login" method="post"> 用户名 : <input type="text" name="email" id="email"/><br> 密码: <input type="password" name="pswd" id="pswd"/><br> 验证码:<input type="text" name="gifCode" id="gifCode"/> <img alt="验证码" src="<%=basePath%>gif/getGifCode"><br> <input type="checkbox" name="rememberMe" />记住我<br> <input style="margin-left: 100px" type="submit" value="登录"/><input style="left: 50px" onclick="register()" type="button" value="注册"/> </form> <h1 style="color: red">${message }</h1> </body>
获取图片是请求后台,所以需要在shiro配置类中配置该url可以直接匿名访问:
/** * 加载ShiroFilter权限控制规则 */ private void loadShiroFilterChain(ShiroFilterFactoryBean factoryBean) { /**下面这些规则配置最好配置到配置文件中*/ Map<String, String> filterChainMap = new LinkedHashMap<String, String>(); //配置记住我或认证通过可以访问的地址 filterChainMap.put("/index", "user"); filterChainMap.put("/", "user"); /** authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器 * org.apache.shiro.web.filter.authc.FormAuthenticationFilter */ //filterChainMap.put("/tUser", "authc");//输入http://localhost:8080/myEra/tUser会跳到登录页面 //filterChainMap.put("/tUser/edit/**", "authc,perms[user:edit]"); // anon:它对应的过滤器里面是空的,什么都没做,可以理解为不拦截 //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 filterChainMap.put("/permission/userInsert", "anon"); filterChainMap.put("/error", "anon"); filterChainMap.put("/tUser/insert","anon"); filterChainMap.put("/gif/getGifCode","anon"); filterChainMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChainMap); }
中的filterChainMap.put("/gif/getGifCode","anon");
后台返回验证码前需把该验证码放入session中:
@Controller @RequestMapping("gif") public class GifCodeController { /** * 获取验证码(Gif版本) * @param response */ @RequestMapping(value="/getGifCode",method= RequestMethod.GET) public void getGifCode(HttpServletResponse response, HttpServletRequest request){ try { response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); response.setContentType("image/gif"); /** * gif格式动画验证码 * 宽,高,位数。 */ Captcha captcha = new GifCaptcha(146,33,4); //输出 captcha.out(response.getOutputStream()); HttpSession session = request.getSession(true); //存入Session session.setAttribute("gifCode",captcha.text().toLowerCase()); } catch (Exception e) { System.err.println("获取验证码异常:"+e.getMessage()); } } }
这里生成验证码的类可以在我的项目里找到。
进入后台登录方法后,直接传入String类型的参数,需要把输入的验证码和session中保存的验证码对比:
@RequestMapping(value="/login",method=RequestMethod.POST) public String login(@Valid User user, boolean rememberMe,String gifCode, BindingResult bindingResult, RedirectAttributes redirectAttributes){ if(bindingResult.hasErrors()){ return "redirect:login"; } String email = user.getEmail(); if(StringUtils.isBlank(user.getEmail()) || StringUtils.isBlank(user.getPswd())){ logger.info("用户名或密码为空! "); redirectAttributes.addFlashAttribute("message", "用户名或密码为空!"); return "redirect:login"; } //判断验证码 if(StringUtils.isBlank(gifCode)){ logger.info("验证码为空了!"); redirectAttributes.addFlashAttribute("message", "验证码不能为空!"); return "redirect:login"; } Session session = SecurityUtils.getSubject().getSession(); String code = (String) session.getAttribute("gifCode"); if(!gifCode.equalsIgnoreCase(code)){ logger.info("验证码错误!"); redirectAttributes.addFlashAttribute("message", "验证码错误!"); return "redirect:login"; } //对密码进行加密后验证 UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), CommonUtils.encrypt(user.getPswd()),rememberMe); //获取当前的Subject Subject currentUser = SecurityUtils.getSubject(); try { //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查 //每个Realm都能在必要时对提交的AuthenticationTokens作出反应 //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法 logger.info("对用户[" + email + "]进行登录验证..验证开始"); currentUser.login(token); logger.info("对用户[" + email + "]进行登录验证..验证通过"); }catch(UnknownAccountException uae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,未知账户"); redirectAttributes.addFlashAttribute("message", "未知账户"); }catch(IncorrectCredentialsException ice){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误的凭证"); redirectAttributes.addFlashAttribute("message", "密码不正确"); }catch(LockedAccountException lae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,账户已锁定"); redirectAttributes.addFlashAttribute("message", "账户已锁定"); }catch(ExcessiveAttemptsException eae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误次数大于5次,账户已锁定"); redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数大于5次,账户已锁定"); }catch (DisabledAccountException sae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,帐号已经禁止登录"); redirectAttributes.addFlashAttribute("message", "帐号已经禁止登录"); }catch(AuthenticationException ae){ //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景 logger.info("对用户[" + email + "]进行登录验证..验证未通过,堆栈轨迹如下"); ae.printStackTrace(); redirectAttributes.addFlashAttribute("message", "用户名或密码不正确"); } //验证是否登录成功 if(currentUser.isAuthenticated()){ logger.info("用户[" + email + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)"); //把当前用户放入session User tUser = permissionService.findByUserEmail(email); session.setAttribute("currentUser",tUser); return "/welcome"; }else{ token.clear(); return "redirect:login"; } // 此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径 // 登陆失败还到login页面 // return "login"; }
页面展示:
当用户验证码输错提示:
(二)登录次数限制
使用redis缓存存储登录的次数,当用户成功登录后,清空该次数:
@Autowired private StringRedisTemplate stringRedisTemplate; //用户登录次数计数 redisKey 前缀 private String SHIRO_LOGIN_COUNT = "shiro_login_count_"; //用户登录是否被锁定 一小时 redisKey 前缀 private String SHIRO_IS_LOCK = "shiro_is_lock_"; @RequestMapping(value="/login",method=RequestMethod.POST) public String login(@Valid User user, boolean rememberMe,String gifCode, BindingResult bindingResult, RedirectAttributes redirectAttributes){ if(bindingResult.hasErrors()){ return "redirect:login"; } String email = user.getEmail(); if(StringUtils.isBlank(user.getEmail()) || StringUtils.isBlank(user.getPswd())){ logger.info("用户名或密码为空! "); redirectAttributes.addFlashAttribute("message", "用户名或密码为空!"); return "redirect:login"; } //判断验证码 if(StringUtils.isBlank(gifCode)){ logger.info("验证码为空了!"); redirectAttributes.addFlashAttribute("message", "验证码不能为空!"); return "redirect:login"; } Session session = SecurityUtils.getSubject().getSession(); String code = (String) session.getAttribute("gifCode"); if(!gifCode.equalsIgnoreCase(code)){ logger.info("验证码错误!"); redirectAttributes.addFlashAttribute("message", "验证码错误!"); return "redirect:login"; } logger.info("进行登录次数验证"); //访问一次,计数一次 ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK+email))){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误次数大于5次,账户已锁定"); redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数大于5次,账户已锁定"); return "redirect:login"; } opsForValue.increment(SHIRO_LOGIN_COUNT+email, 1); //计数大于3时,设置用户被锁定一小时 if(Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT+email))>=5){ opsForValue.set(SHIRO_IS_LOCK+email, "LOCK"); stringRedisTemplate.expire(SHIRO_IS_LOCK+email, 1, TimeUnit.HOURS); } //对密码进行加密后验证 UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), CommonUtils.encrypt(user.getPswd()),rememberMe); //获取当前的Subject Subject currentUser = SecurityUtils.getSubject(); try { //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查 //每个Realm都能在必要时对提交的AuthenticationTokens作出反应 //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法 logger.info("对用户[" + email + "]进行登录验证..验证开始"); currentUser.login(token); logger.info("对用户[" + email + "]进行登录验证..验证通过"); }catch(UnknownAccountException uae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,未知账户"); redirectAttributes.addFlashAttribute("message", "未知账户"); }catch(IncorrectCredentialsException ice){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误的凭证"); redirectAttributes.addFlashAttribute("message", "密码不正确"); }catch(LockedAccountException lae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,账户已锁定"); redirectAttributes.addFlashAttribute("message", "账户已锁定"); }catch(ExcessiveAttemptsException eae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误次数大于5次,账户已锁定"); redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数大于5次,账户已锁定"); }catch (DisabledAccountException sae){ logger.info("对用户[" + email + "]进行登录验证..验证未通过,帐号已经禁止登录"); redirectAttributes.addFlashAttribute("message", "帐号已经禁止登录"); }catch(AuthenticationException ae){ //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景 logger.info("对用户[" + email + "]进行登录验证..验证未通过,堆栈轨迹如下"); ae.printStackTrace(); redirectAttributes.addFlashAttribute("message", "用户名或密码不正确"); } //验证是否登录成功 if(currentUser.isAuthenticated()){ logger.info("用户[" + email + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)"); //清空登录计数 opsForValue.set(SHIRO_LOGIN_COUNT+email, "0"); //设置未锁定状态 opsForValue.set(SHIRO_IS_LOCK+email,"UNLOCK"); //把当前用户放入session User tUser = permissionService.findByUserEmail(email); session.setAttribute("currentUser",tUser); return "/welcome"; }else{ token.clear(); return "redirect:login"; } // 此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径 // 登陆失败还到login页面 // return "login"; }
当用户名或密码输错5次后,提示:
验证码和登录次数功能参考博客:http://z77z.oschina.io/