Spring Boot2(十五):Shiro记住我rememberMe、验证码Kaptcha

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

Remember Me记住我:用户的登陆状态会不会由于浏览器的关闭而失效,直到Cookie失效。关闭浏览器后,再次访问登陆后的页面能够不用登陆。由于用Cookie实现,故只在同一浏览器中有效。css

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

1、记住我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访问。java

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

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

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

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

/**
 * 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";
}
复制代码

启动项目,进行测试能够看到效果以下:

登陆操做

2、验证码Kaptcha

kaptcha 是一个很是实用的验证码生成工具。有了它,你能够生成各类样式的验证码,由于它是可配置的。kaptcha工做的原理是调用 com.google.code.kaptcha.servlet.KaptchaServlet,生成一个图片。同时将生成的验证码字符串放到 HttpSession中。

Kaptcha官网:code.google.com/archive/p/k…

使用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中获取后台生成的验证码。

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

//一、检验验证码
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验证码校验

3、源码

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


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

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

谢谢,一块儿学习,共同进步,成为优秀的人

img
相关文章
相关标签/搜索