为了防止通过程序进行暴力登录等, 系统在登录时都会增加验证码用来分区是人为登录还是使用程序登录.
验证码的原理很简单: 在用户访问登录页面时请求服务器生成验证码, 服务器将生成的验证码保存至SESSION后生成验证码图片并显示在登录页面, 由于程序识别图片内容的成功率较低, 而人可以很快识别图片中的内容, 以此减少非人为的登录等非法操作.
技术发展到今天, 程序识别图片内容的成功率越来越高, 验证码的交互形式也越来越多. 本文已最简单的图片验证码为例, 讲解Shiro中如何实现集成验证码
Kaptcha
是谷歌开源的一个验证码插件, 通过在 Web.xml
中配置内置的 Servlet
即可实现生成验证码.
"com.github.penggle:kaptcha:2.3.2" 复制代码
<!-- 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的值和图片内容相同
<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" /> 复制代码
访问登录页, 会显示验证码输入框及验证码图片
在[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; } } 复制代码
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); } } 复制代码
FormAuthenticationFilter
父类中的Shiro内置的登录方法中 createToken
后, Shiro在创建Token时发现方法被重写, 便会执行之定义的创建Token方法 CaptchaToken
时一定要设置用户名和密码, 否则 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中的登录逻辑 // ----------------------------------------------------------------- } 复制代码
// 使用自定义的表单认证过滤器 // 该过滤器中只是重写了Shiro的创建Token方法(增加了验证码) authc(CaptchaFormAuthenticationFilter) 复制代码
// 配置URL规则 // 有请求访问时Shiro会根据此规则找到对应的过滤器处理 filterChainDefinitionMap = [ "/kaptcha" : "anon", // 验证码不需要登录即可访问 "/kaptcha/get" : "anon", // 获取验证码不需要登录即可访问 "/login_success.jsp" : "anon", // 登录成功页不需要认证 "/**": "authc" // 其余所有页面需要认证(使用自定义的authc为过滤器) ] 复制代码
authc
为过滤器名称, 未声明时使用Shiro自带的 FormAuthenticationFilter
, 已声明时使用配置文件中声明的过滤器 <!-- 验证码异常 --> <!-- 在登录的Realm中验证码校验错误时会抛出相关异常 --> <c:if test="${shiroLoginFailure == 'com.atd681.shiro.kaptcha.CaptchaEmptyException'}">验证码为空</c:if> <c:if test="${shiroLoginFailure == 'com.atd681.shiro.kaptcha.CaptchaErrorException'}">验证码不正确</c:if> 复制代码