接着上次学习的《Spring Boot2(十二):手摸手教你搭建Shiro安全框架》,实现了Shiro的认证和授权。今天继续在这个基础上学习Shiro实现功能记住我rememberMe,以及登录时验证码Kaptcha。

Remember Me记住我:用户的登录状态会不会因为浏览器的关闭而失效,直到Cookie失效。关闭浏览器后,再次访问登录后的页面可以不用登录。因为用Cookie实现,故只在同一浏览器中有效。

Kaptcha验证码:是谷歌开源的验证码插件,实现登录的验证码验证拦截。

一、记住我rememberMe

用户的登录状态会不会因为浏览器的关闭而失效,直到Cookie失效。关闭浏览器后,再次访问登录后的页面可以不用登录。因为用Cookie实现,故只在同一浏览器中有效。

修改ShiroConfig

/**
* 路径过滤规则
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/index");
// 拦截器
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
// 对静态资源设置匿名访问
map.put("/static/**", "anon");
map.put("/css/**", "anon");
map.put("/js/**", "anon");

// 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
// 进行身份认证后才能访问
// authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
// user指的是用户认证通过或者配置了Remember Me记住用户登录状态后可访问
map.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}

因为对登录页面做了一些样式,新增了静态资源文件static,这时候遇到了坑,页面引用的jscss都无效了,然后发现时因为被拦截了,我们需要在Shiro的拦截器中允许对静态资源的匿名anon访问。

注意到将ShiroFilterFactoryBeanmap.put("/**", "authc");更改为map.put("/**", "user");user是指用户认证通过或配置了RememberMe记住用户登录状态后可访问。

解决过程查阅了一些资料,不光光只对cssjs的放开,还需要对static也放开

对静态资源的拦截相关问题可以参照这里了解学习一下:Spring Boot Shiro无法访问JS/CSS/IMG+自定义Filter无法访问完美方案

回来继续,调用SimpleCookie,配置Cookie的基本属性:名称和过期时间。

/**
* cookie对象
* @return
*/
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进行管理起来。

/**
* cookie管理对象
* rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
* @return
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}

接下来将cookie管理对象设置到SecurityManager中:

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(authRealm());
// 用户授权/认证信息Cache, 采用EhC//注入记住我管理器
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加密处理获得密文。

登录接口

/**
* 登录操作
* @param account
* @param password
* @param rememberMe
* @return
*/
@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);
}
}

注销接口

/**
* 登出
* @return
*/
@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");
//session的key
//properties.setProperty("kaptcha.session.key", "code");
//长度
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);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
// create the text for the image
String capText = captchaProducer.createText();
//将验证码存到session
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中获取后台生成的验证码。

最后通过比较前端输入的验证码和后台生成的是否一致。

//1、检验验证码
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);
//还可以读取一次后把验证码清空,这样每次登录都必须获取验证码
//session.removeAttribute("_come");
if(!validateCode.equals(v)){
return ResponseCode.error(StatusEnums.VALIDATECODE_ERROR);
}

下图是登录校验验证码的debug过程。

kaptcha验证码校验

三、源码

源码地址:spring-boot-23-shiro-remember
欢迎star、fork,给作者一些鼓励


菜鸟也要成为架构师,一起努力

欢迎关注我微信公众号【鸟不拉诗】

谢谢,一起学习,共同进步,成为优秀的人。

微信公众号:鸟不拉诗