接着上次学习的《Spring Boot2(十二):手摸手教你搭建Shiro安全框架》,实现了Shiro的认证和授权。今天继续在这个基础上学习Shiro实现功能记住我rememberMe,以及登录时验证码Kaptcha。
Remember Me记住我:用户的登录状态会不会因为浏览器的关闭而失效,直到Cookie失效。关闭浏览器后,再次访问登录后的页面可以不用登录。因为用Cookie实现,故只在同一浏览器中有效。
Kaptcha验证码:是谷歌开源的验证码插件,实现登录的验证码验证拦截。
一、记住我rememberMe
用户的登录状态会不会因为浏览器的关闭而失效,直到Cookie失效。关闭浏览器后,再次访问登录后的页面可以不用登录。因为用Cookie实现,故只在同一浏览器中有效。
修改ShiroConfig
@Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/index"); LinkedHashMap<String, String> map = new LinkedHashMap<>(); map.put("/static/**", "anon"); map.put("/css/**", "anon"); map.put("/js/**", "anon");
map.put("/**", "user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; }
|
因为对登录页面做了一些样式,新增了静态资源文件static,这时候遇到了坑,页面引用的js
和css
都无效了,然后发现时因为被拦截了,我们需要在Shiro的拦截器中允许对静态资源的匿名anon
访问。
注意到将ShiroFilterFactoryBean
的map.put("/**", "authc");
更改为map.put("/**", "user");
user是指用户认证通过或配置了RememberMe记住用户登录状态后可访问。
解决过程查阅了一些资料,不光光只对css
和js
的放开,还需要对static
也放开
对静态资源的拦截相关问题可以参照这里了解学习一下:Spring Boot Shiro无法访问JS/CSS/IMG+自定义Filter无法访问完美方案
回来继续,调用SimpleCookie,配置Cookie的基本属性:名称和过期时间。
public SimpleCookie rememberMeCookie() { // 设置cookie名称,对应login.html页面的<input type="checkbox" name="rememberMe"/> SimpleCookie cookie = new SimpleCookie("rememberMe"); // 设置cookie的过期时间,单位为秒,这里为一天 cookie.setMaxAge(86400); return cookie; }
|
SimleCookie参数中的名称为页面的name标签属性名称。
实现了Cookie对象属性配置,还需要通过CookieRememberMeManager
进行管理起来。
public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; }
|
接下来将cookie管理对象设置到SecurityManager
中:
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(authRealm()); securityManager.setRememberMeManager(rememberMeManager()); return securityManager; }
|
加密处理
《Spring Boot2(十二):手摸手教你搭建Shiro安全框架》这个项目中用的明文,这里我们升个级,使用MD5加密
新建MD5加密工具类。
public class MD5Utils {
private static final String SALT = "niaobulashi";
private static final String ALGORITH_NAME = "md5";
private static final int HASH_ITERATIONS = 2;
public static String encrypt(String pwd) { String newPassword = new SimpleHash(ALGORITH_NAME, pwd, ByteSource.Util.bytes(SALT), HASH_ITERATIONS).toHex(); return newPassword; }
public static String encrypt(String username, String pwd) { String newPassword = new SimpleHash(ALGORITH_NAME, pwd, ByteSource.Util.bytes(username + SALT), HASH_ITERATIONS).toHex(); return newPassword; } public static void main(String[] args) { System.out.println("MD5加密后的密文为:" + MD5Utils.encrypt("root", "root")); } }
|
其中SALT
是加密的盐,可自行定义。
main方法中,根据登录名和密码明文,输出最终加密的密文,将输出内容粘贴到我们的数据库中,待后续登录时使用。
新增登录页面和主页面
登录页login.html
添加Remember Me checkbox
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登录</title> <link rel="stylesheet" th:href="@{/static/css/login.css}" type="text/css"> <script th:src="@{/static/js/jquery-1.11.1.min.js}"></script> </head> <body> <div class="login-page"> <div class="form"> <input type="text" placeholder="用户名" name="account" required="required"/> <input type="password" placeholder="密码" name="password" required="required"/> <p><input type="checkbox" name="rememberMe"/>记住我</p> <button onclick="login()">登录</button> </div> </div> </body> <script th:inline="javascript">var ctx = [[@{/}]];</script> <script th:inline="javascript"> function login() { var account = $("input[name='account']").val(); var password = $("input[name='password']").val(); var rememberMe = $("input[name='rememberMe']").is(':checked'); $.ajax({ type: "post", url: ctx + "login", data: { "account": account, "password": password, "rememberMe": rememberMe }, success: function(r) { if (r.code == 100) { location.href = ctx + 'index'; } else { alert(r.message); } } }); } </script> </html>
|
静态资源js和css可以在源码中查看

首页index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <p>你好![[${user.getUsername()}]]</p> <a th:href="@{/logout}">注销</a> </body> </html>
|
Controller层
在原来的基础上,新增参数rememberMe,同时对用户名和明文密码进行MD5加密处理获得密文。
登录接口
@PostMapping("/login") @ResponseBody public ResponseCode login(String account, String password, Boolean rememberMe) { logger.info("登录请求-start"); password = MD5Utils.encrypt(account, password); Subject userSubject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(account, password, rememberMe); try { userSubject.login(token); return ResponseCode.success(); } catch (UnknownAccountException e) { return ResponseCode.error(StatusEnums.ACCOUNT_UNKNOWN); } catch (DisabledAccountException e) { return ResponseCode.error(StatusEnums.ACCOUNT_IS_DISABLED); } catch (IncorrectCredentialsException e) { return ResponseCode.error(StatusEnums.INCORRECT_CREDENTIALS); } catch (AuthenticationException e) { return ResponseCode.error(StatusEnums.AUTH_ERROR); } catch (Throwable e) { e.printStackTrace(); return ResponseCode.error(StatusEnums.SYSTEM_ERROR); } }
|
注销接口
@GetMapping("/logout") public String logout() { getSubject().logout(); return "login"; }
|
启动项目,进行测试可以看到效果如下:

二、验证码Kaptcha
kaptcha 是一个非常实用的验证码生成工具。有了它,你可以生成各种样式的验证码,因为它是可配置的。kaptcha工作的原理是调用 com.google.code.kaptcha.servlet.KaptchaServlet,生成一个图片。同时将生成的验证码字符串放到 HttpSession中。
Kaptcha官网:https://code.google.com/archive/p/kaptcha/
使用kaptcha可以方便的配置:
- 验证码的字体
- 验证码字体的大小
- 验证码字体的字体颜色
- 验证码内容的范围(数字,字母,中文汉字!)
- 验证码图片的大小,边框,边框粗细,边框颜色
- 验证码的干扰线(可以自己继承com.google.code.kaptcha.NoiseProducer写一个自定义的干扰线)
- 验证码的样式(鱼眼样式、3D、普通模糊……当然也可以继承com.google.code.kaptcha.GimpyEngine自定义样式)
kaptcha配置详解
kaptcha对象属性 | 作用 | 默认值 |
---|
kaptcha.border | 是否有边框 | 默认为true |
kaptcha.border.color | 边框颜色 | 默认为Color.BLACK |
kaptcha.border.thickness | 边框粗细度 | 默认为1 |
kaptcha.producer.impl | 验证码生成器 | 默认为DefaultKaptcha |
kaptcha.textproducer.impl | 验证码文本生成器 | 默认为DefaultTextCreator |
kaptcha.textproducer.char.string | 验证码文本字符内容范围 | 默认为abcde2345678gfynmnpwx |
kaptcha.textproducer.char.length | 验证码文本字符长度 | 默认为5 |
kaptcha.textproducer.font.names | 验证码文本字体样式 | 宋体,楷体,微软雅黑,默认为new Font(“Arial”, 1, fontSize), new Font(“Courier”, 1, fontSize) |
kaptcha.textproducer.font.size | 验证码文本字符大小 | 默认为40 |
kaptcha.textproducer.font.color | 验证码文本字符颜色 | 默认为Color.BLACK |
kaptcha.textproducer.char.space | 验证码文本字符间距 | 默认为2 |
kaptcha.noise.impl | 验证码噪点生成对象 | 默认为DefaultNoise |
kaptcha.noise.color | 验证码噪点颜色 | 默认为Color.BLACK |
kaptcha.obscurificator.impl | 验证码样式引擎 | 默认为WaterRipple |
kaptcha.word.impl | 验证码文本字符渲染 | 默认为DefaultWordRenderer |
kaptcha.background.impl | 验证码背景生成器 | 默认为DefaultBackground |
kaptcha.background.clear.from | 验证码背景颜色渐进 | 默认为Color.LIGHT_GRAY |
kaptcha.background.clear.to | 验证码背景颜色渐进 | 默认为Color.WHITE |
kaptcha.image.width | 验证码图片宽度 | 默认为200 |
kaptcha.image.height | 验证码图片高度 | 默认为50 |
添加maven依赖
<!--验证码--> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
|
新增验证码图片样式配置器
具体配置可以参考上面的kaptche配置详情,针对不同的常见配置。
@Configuration public class KaptchaConfig {
@Bean(name="captchaProducer") public DefaultKaptcha getKaptchaBean(){ DefaultKaptcha defaultKaptcha=new DefaultKaptcha(); Properties properties=new Properties(); properties.setProperty("kaptcha.textproducer.char.string", "23456789"); properties.setProperty("kaptcha.border.color", "245,248,249"); properties.setProperty("kaptcha.textproducer.font.color", "black"); properties.setProperty("kaptcha.textproducer.char.space", "1"); properties.setProperty("kaptcha.image.width", "100"); properties.setProperty("kaptcha.image.height", "35"); properties.setProperty("kaptcha.textproducer.font.size", "30"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); Config config=new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
|
新增图片验证码Controller层
是一个创建文件图片流的过程,使用ServletOutPutStream输出最后的图片。
开头声明的@Resource(name = "captchaProducer")
,是验证码图片样式配置器启动时配置的Bean:captchaProducer
。
@Controller @RequestMapping("/captcha") public class KaptchaController {
private static final Logger logger = LoggerFactory.getLogger(KaptchaController.class);
@Resource(name = "captchaProducer") private Producer captchaProducer;
@GetMapping("/captchaImage") public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception { ServletOutputStream out = response.getOutputStream(); try { HttpSession session = request.getSession(); response.setDateHeader("Expires", 0); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); String capText = captchaProducer.createText(); session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText); logger.info(capText); BufferedImage bi = captchaProducer.createImage(capText); out = response.getOutputStream(); ImageIO.write(bi, "jpg", out);
out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } }
|
注意最后都需要将流关闭out.close()
放开图片验证码的拦截
重启会发现,图片验证码的接口请求无法访问,还是跳转到了localhost:8081/login登录页面
因为Shiro配置的拦截器没有放开,需要再ShiroConfig
中允许匿名访问改请求资源
map.put("/captcha/captchaImage**", "anon");
|
登录页面添加图片验证码
<div class="login-page"> <div class="form"> <input type="text" placeholder="用户名" name="account" required="required"/> <input type="password" placeholder="密码" name="password" required="required"/> <p> <label>验证码<br/> <input type="text" name="validateCode" id="validateCode" class="validateCode" required="required"/> <a href="javascript:void(0);"> <img src="/captcha/captchaImage" onclick="this.src='/captcha/captchaImage?'+Math.random()"/> </a> </label> </p> <br> <p><input type="checkbox" name="rememberMe"/>记住我</p> <button onclick="login()">登录</button> </div> </div>
|
上面div
为body的全部部分
我在请求/captcha/captchaImage
后面添加随机值Math.random()
。是因为客户浏览器会缓存URL相同的资源,故使用随机数来重新请求。这和前端上线时,请求后缀都会变更一个版本号一样,不需要让客户手动刷新浏览器就可以获取最新资源一样。

修改登录请求接口
主要是验证后台生成的验证码,与前台输入的验证码进行比较,验证是否相同
这里只粘贴出验证码验证的逻辑,源码在文章最后。
可以看出validateCode
是前端请求过来的参数,先校验是否为空。
然后从session中获取后台生成的验证码。
最后通过比较前端输入的验证码和后台生成的是否一致。
if(validateCode == null || validateCode == ""){ return ResponseCode.error(StatusEnums.PARAM_NULL); } Session session = SecurityUtils.getSubject().getSession();
validateCode = validateCode.toLowerCase(); String v = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(!validateCode.equals(v)){ return ResponseCode.error(StatusEnums.VALIDATECODE_ERROR); }
|
下图是登录校验验证码的debug过程。

三、源码
源码地址:spring-boot-23-shiro-remember
欢迎star、fork,给作者一些鼓励
菜鸟也要成为架构师,一起努力
欢迎关注我微信公众号【鸟不拉诗】
谢谢,一起学习,共同进步,成为优秀的人。
