Shiro-集成验证码

2,800 阅读7分钟

0) 前言

为了防止通过程序进行暴力登录等, 系统在登录时都会增加验证码用来分区是人为登录还是使用程序登录.

验证码的原理很简单: 在用户访问登录页面时请求服务器生成验证码, 服务器将生成的验证码保存至SESSION后生成验证码图片并显示在登录页面, 由于程序识别图片内容的成功率较低, 而人可以很快识别图片中的内容, 以此减少非人为的登录等非法操作.

技术发展到今天, 程序识别图片内容的成功率越来越高, 验证码的交互形式也越来越多. 本文已最简单的图片验证码为例, 讲解Shiro中如何实现集成验证码


1) 集成Kaptcha

Kaptcha是谷歌开源的一个验证码插件, 通过在Web.xml中配置内置的Servlet即可实现生成验证码.

  • 引入Kaptcha类库
"com.github.penggle:kaptcha:2.3.2"
  • Web.xml增加Kaptcha内置Servlet配置
<!-- Kaptcha Servlet -->
<servlet>
	<servlet-name>Kaptcha</servlet-name>
	<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
	<!-- 参数: 验证码图片高度 -->
	<init-param>
		<param-name>kaptcha.image.width</param-name>
		<param-value>200</param-value>
	</init-param>
</servlet>
<servlet-mapping>
	<servlet-name>Kaptcha</servlet-name>
	<url-pattern>/kaptcha</url-pattern>
</servlet-mapping>

Kaptcha内置的属性可以使用init-param进行配置, 下面列出常用的配置信息, 更多请参考官方文档:

属性 含义
kaptcha.border 是否有边框, 默认有
kaptcha.border.color 边框颜色, 使用RGB值或white, red等颜色单次, 默认黑色
kaptcha.border.thickness 边框宽度, 默认1px
kaptcha.image.width 验证码图片宽度
kaptcha.image.height 验证码图片高度
kaptcha.textproducer.char.string 验证码字符内容, 默认abcde2345678gfynmnpwx
kaptcha.textproducer.char.length 验证码字符数量, 默认5
kaptcha.textproducer.font.names 字体名称, 默认Arial/Courier
kaptcha.textproducer.font.size 字体大小, 默认40px
kaptcha.textproducer.font.color 字体颜色, 默认黑色
kaptcha.session.key 保存SESSION时的KEY, 默认KAPTCHA_SESSION_KEY
kaptcha.background.clear.from/to 背景渐变色起始/终止颜色, RGB色值, 默认浅灰/白
  • 新建Controller, 从SESSION获取验证码的值. Kaptcha在SESSION中的KEY为KAPTCHA_SESSION_KEY
/**
 * 获取验证码
 * 
 * @author atd681
 * @since 2018年8月13日
 */
@GetMapping("/kaptcha/get")
@ResponseBody
public String getKaptcha(HttpSession session) {
    // Kaptcha生成验证后保存SESSION中的KEY为KAPTCHA_SESSION_KEY
    return (String) session.getAttribute("KAPTCHA_SESSION_KEY");
}

验证码添加完成, 启动项目:

  • 访问http://localhost:6789/kaptcha, 即可看到验证码图片.

  • 访问http://localhost:6789/kaptcha/get, 两次请求在同一个SESSION中, 验证码在SESSION的值和图片内容相同


2) 登录页显示验证码

<form action="/login" method="post">
	<input type="text" name="username" placeholder="用户名" value="" />
	<input type="password" name="password" placeholder="密码" value="" />
	<!-- 增加验证码输入框 -->
	<input type="text" name="captchaCode" placeholder="验证码" value="" />
	<input type="submit" value="立即登录" />
</form>
<!-- 验证码,请求地址为在Web.xml中配置的Kaptcha内置的Servlet-->
<!-- Kaptcha Servlet生成验证码保存至SESSION并将图片返回 -->
<img src="/kaptcha" />

访问登录页, 会显示验证码输入框及验证码图片


3) 扩展Token

[Shiro-认证]中讲到, 登录验证是否合法是在Realm实现的, 因此验证码也放到Realm中进行验证, Shiro会将登录提交的用户名和密码封装成UsernamePasswordToken传递至Realm中. 查看UsernamePasswordToken发现该类中并保存无验证码的字段, 因此需要重新定义一个Token可以保存验证码.

private String username;
private char[] password;
private boolean rememberMe = false;
private String host;

新建CaptchaToken继承UsernamePasswordToken, 在CaptchaToken增加验证码字段即可.

/**
 * 扩展Shiro登录表单Token,增加验证码字段
 */
public class CaptchaToken extends UsernamePasswordToken {

    // 序列化ID
    private static final long serialVersionUID = -2804050723838289739L;

    // 验证码
    private String captchaCode;

    /**
     * 构造函数
     * 用户名和密码是登录必须的,因此构造函数中包含两个字段
     */
    public CaptchaToken(String username, String password, String captchaCode) {
        // 父类UsernamePasswordToken的构造函数,后两个参数暂不需要, 不设置
        super(username, password, false, "");
        this.captchaCode = captchaCode;
    }

    /**
     * 获取验证码
     */
    public String getCaptchaCode() {
        return captchaCode;
    }

}

4) Shiro使用CaptchaToken

Shiro创建Token时默认使用UsernamePasswordToken, 在FormAuthenticationFilter类的createToken方法中创建.

新建CaptchaFormAuthenticationFilter继承FormAuthenticationFilter并重写createToken方法, 使用CaptchaToken并设置验证码.

/**
 * 自定义认证过滤器
 */
public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter {

    /**
     * 构造Token,重写Shiro构造Token的方法,增加验证码
     */
    @Override
    protected AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response) {
        // 获取登录请求中用户输入的验证码
        String captchaCode = request.getParameter("captchaCode");
        // 返回带验证码的Token,Token会被传入Realm, 在Realm中可以取得验证码
        return new CaptchaToken(username, password, captchaCode);
    }

}
  • 创建Token是在FormAuthenticationFilter父类中的Shiro内置的登录方法中
  • 登录页面中已经传入验证码, 通过Request直接获取即可.
  • 重写createToken后, Shiro在创建Token时发现方法被重写, 便会执行之定义的创建Token方法
  • 使用CaptchaToken时一定要设置用户名和密码, 否则Realm中无法获取用户名密码

5) Realm中增加验证码校验

[Shiro-认证]中讲到, Shiro登录失败的错误是以异常的方式抛出, Shiro提供常见的错误异常, 但并提供没有验证码错误异常. 我们需要自定义两个和验证码相关的异常

  • 验证码为空: CaptchaEmptyException

  • 验证码错误: CaptchaErrorException

/**
 * 自定义验证码为空异常
 * AuthenticationException为Shiro认证错误的异常,不同错误类型继承该异常即可
 */
public class CaptchaEmptyException extends AuthenticationException {
}

/**
 * 自定义验证码错误异常
 * AuthenticationException为Shiro认证错误的异常,不同错误类型继承该异常即可
 */
public class CaptchaErrorException extends AuthenticationException {
}

在Realm中增加验证码非空和正确性验证, 当验证失败时抛出上述异常. 如果登录过程中抛出了父类为AuthenticationException的异常, Shiro认为登录失败. 记录异常信息并执行登录失败逻辑.

// 获取用户信息的方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    // 在自定义的认证过滤器中将验证码保存至KaptchaCodeToken中
    // 此处的Token就是认证过滤器中实例化的Token,可以直接强制转换
    CaptchaToken captchaToken = (CaptchaToken) token;

    // 获取用户在登录页面输入的验证码
    String loginCaptcha = captchaToken.getCaptchaCode();

    // 验证码未输入
    if (loginCaptcha == null || "".equals(loginCaptcha)) {
        // 抛出自定义异常(继承AuthenticationException), Shiro会捕获AuthenticationException异常
        // 发现该异常时认为登录失败,执行登录失败逻辑,登录失败页中可以判断如果是CaptchaEmptyException时为验证码为空
        throw new CaptchaEmptyException();
    }

    // 获取SESSION中的验证码
    // Kaptcha在生成验证码时会将验证码放入SESSION中
    // 默认KEY为KAPTCHA_SESSION_KEY, 可以在Web.xml中配置
    String sessionCaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute("KAPTCHA_SESSION_KEY");

    // 比较登录输入的验证码和SESSION保存的验证码是否一致
    if (!loginCaptcha.equals(sessionCaptcha)) {
        // 抛出自定义异常(继承AuthenticationException), Shiro会捕获AuthenticationException异常
        // 发现该异常时认为登录失败,执行登录失败逻辑,登录失败页中可以判断如果是CaptchaEmptyException时为验证码错误
        throw new CaptchaErrorException();
    }

    // -----------------------------------------------------------------
    // 以下是atd681-shiro-authc中的登录逻辑
    // -----------------------------------------------------------------


}

6) Shiro配置自定义过滤器

  • 声明自定义过滤器
// 使用自定义的表单认证过滤器
// 该过滤器中只是重写了Shiro的创建Token方法(增加了验证码)
authc(CaptchaFormAuthenticationFilter)
  • 配置所有请求使用自定义过滤器
// 配置URL规则
// 有请求访问时Shiro会根据此规则找到对应的过滤器处理
filterChainDefinitionMap = [
    "/kaptcha" : "anon", // 验证码不需要登录即可访问
    "/kaptcha/get" : "anon", // 获取验证码不需要登录即可访问
    "/login_success.jsp" : "anon", // 登录成功页不需要认证
    "/**": "authc" // 其余所有页面需要认证(使用自定义的authc为过滤器)
]
  • authc为过滤器名称, 未声明时使用Shiro自带的FormAuthenticationFilter, 已声明时使用配置文件中声明的过滤器
  • 获取验证码URL不需要认证

7) 登录失败页增加提示信息

<!-- 验证码异常 -->
<!-- 在登录的Realm中验证码校验错误时会抛出相关异常 -->
<c:if test="${shiroLoginFailure == 'com.atd681.shiro.kaptcha.CaptchaEmptyException'}">验证码为空</c:if>
<c:if test="${shiroLoginFailure == 'com.atd681.shiro.kaptcha.CaptchaErrorException'}">验证码不正确</c:if>

8) 示例代码

至此, 基于Shiro的验证码示例配置完成.

  • 示例代码地址: https://github.com/atd681/alldemo

  • 示例项目名称: atd681-shiro-kaptcha