鸟不拉屎

Spring Boot2(十五):Shiro记住我rememberMe、验证码Kaptcha
仓库地址:spring-boot-learning欢迎star、fork,给作者一些鼓励接着上次学习的《Sprin...
扫描右侧二维码阅读全文
25
2019/07

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

仓库地址:spring-boot-learning
欢迎star、fork,给作者一些鼓励

接着上次学习的《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,给作者一些鼓励


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

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

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

文章名: 《Spring Boot2(十五):Shiro记住我rememberMe、验证码Kaptcha》
文章链接:https://niaobulashi.com/archives/shiro-rememberme-kaptcha.html
除特别注明外,文章均为鸟不拉屎原创,转载时请注明本文出处及文章链接
公众号:鸟不拉屎(一个正在努力Coding的未来架构师)
Last modification:July 26th, 2019 at 05:44 pm

Leave a Comment

2 comments

  1. Lzm

    好东西。。。

    1. 鸟不拉屎
      @Lzm


      哎,只是些皮毛
      最近在学并发编程。
      我日,懵逼啊,好痛苦。。。。