目录结构:
- 概述
- 扩展shiro认证
- 验证码工具
- 验证码servlet
- 配置文件修改
- 修改登录页面
- 测试验证
[一]、概述
本文简单讲述在web应用整合shiro后,如何实现登录验证码认证的功能。
[二]、扩展shiro的认证
创建验证码异常类:CaptchaException.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package com.micmiu.modules.support.shiro; import org.apache.shiro.authc.AuthenticationException; /** * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class CaptchaException extends AuthenticationException { private static final long serialVersionUID = 1L; public CaptchaException() { super(); } public CaptchaException(String message, Throwable cause) { super(message, cause); } public CaptchaException(String message) { super(message); } public CaptchaException(Throwable cause) { super(cause); } } |
扩展默认的用户认证的bean为:UsernamePasswordCaptchaToken.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package com.micmiu.modules.support.shiro; import org.apache.shiro.authc.UsernamePasswordToken; /** * extends UsernamePasswordToken for captcha * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class UsernamePasswordCaptchaToken extends UsernamePasswordToken { private static final long serialVersionUID = 1L; private String captcha; public String getCaptcha() { return captcha; } public void setCaptcha(String captcha) { this.captcha = captcha; } public UsernamePasswordCaptchaToken() { super(); } public UsernamePasswordCaptchaToken(String username, char[] password, boolean rememberMe, String host, String captcha) { super(username, password, rememberMe, host); this.captcha = captcha; } } |
扩展原始默认的过滤为:FormAuthenticationCaptchaFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package com.micmiu.modules.support.shiro; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.util.WebUtils; /** * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class FormAuthenticationCaptchaFilter extends FormAuthenticationFilter { public static final String DEFAULT_CAPTCHA_PARAM = "captcha"; private String captchaParam = DEFAULT_CAPTCHA_PARAM; public String getCaptchaParam() { return captchaParam; } protected String getCaptcha(ServletRequest request) { return WebUtils.getCleanParam(request, getCaptchaParam()); } protected AuthenticationToken createToken( ServletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); String captcha = getCaptcha(request); boolean rememberMe = isRememberMe(request); String host = getHost(request); return new UsernamePasswordCaptchaToken(username, password.toCharArray(), rememberMe, host, captcha); } } |
修改shiro认证逻辑:ShiroDbRealm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
package com.micmiu.framework.web.v1.system.service; import java.io.Serializable; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.cache.Cache; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import com.micmiu.framework.web.v1.system.entity.Role; import com.micmiu.framework.web.v1.system.entity.User; import com.micmiu.modules.captcha.CaptchaServlet; import com.micmiu.modules.support.shiro.CaptchaException; import com.micmiu.modules.support.shiro.UsernamePasswordCaptchaToken; /** * 演示用户和权限的认证,使用默认 的SimpleCredentialsMatcher * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class ShiroDbRealm extends AuthorizingRealm { private UserService userService; /** * 认证回调函数, 登录时调用. */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordCaptchaToken token = (UsernamePasswordCaptchaToken) authcToken; String username = token.getUsername(); if (username == null) { throw new AccountException( "Null usernames are not allowed by this realm."); } // 增加判断验证码逻辑 String captcha = token.getCaptcha(); String exitCode = (String) SecurityUtils.getSubject().getSession() .getAttribute(CaptchaServlet.KEY_CAPTCHA); if (null == captcha || !captcha.equalsIgnoreCase(exitCode)) { throw new CaptchaException("验证码错误"); } User user = userService.getUserByLoginName(username); if (null == user) { throw new UnknownAccountException("No account found for user [" + username + "]"); } return new SimpleAuthenticationInfo(new ShiroUser(user.getLoginName(), user.getName()), user.getPassword(), getName()); } /** * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用. */ @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { ShiroUser shiroUser = (ShiroUser) principals.fromRealm(getName()) .iterator().next(); User user = userService.getUserByLoginName(shiroUser.getLoginName()); if (user != null) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for (Role role : user.getRoleList()) { // 基于Permission的权限信息 info.addStringPermissions(role.getAuthList()); } return info; } else { return null; } } /** * 更新用户授权信息缓存. */ public void clearCachedAuthorizationInfo(String principal) { SimplePrincipalCollection principals = new SimplePrincipalCollection( principal, getName()); clearCachedAuthorizationInfo(principals); } /** * 清除所有用户授权信息缓存. */ public void clearAllCachedAuthorizationInfo() { Cache<Object, AuthorizationInfo> cache = getAuthorizationCache(); if (cache != null) { for (Object key : cache.keys()) { cache.remove(key); } } } @Autowired public void setUserService(UserService userService) { this.userService = userService; } /** * 自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息. */ public static class ShiroUser implements Serializable { private static final long serialVersionUID = -1748602382963711884L; private String loginName; private String name; public ShiroUser(String loginName, String name) { this.loginName = loginName; this.name = name; } public String getLoginName() { return loginName; } /** * 本函数输出将作为默认的<shiro:principal/>输出. */ @Override public String toString() { return loginName; } public String getName() { return name; } } } |
[三]、验证码工具类
CaptchaUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
package com.micmiu.modules.captcha; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.util.Random; import javax.imageio.ImageIO; /** * 验证码工具类 * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class CaptchaUtil { // 随机产生的字符串 private static final String RANDOM_STRS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static final String FONT_NAME = "Fixedsys"; private static final int FONT_SIZE = 18; private Random random = new Random(); private int width = 80;// 图片宽 private int height = 25;// 图片高 private int lineNum = 50;// 干扰线数量 private int strNum = 4;// 随机产生字符数量 /** * 生成随机图片 */ public BufferedImage genRandomCodeImage(StringBuffer randomCode) { // BufferedImage类是具有缓冲区的Image类 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); // 获取Graphics对象,便于对图像进行各种绘制操作 Graphics g = image.getGraphics(); // 设置背景色 g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); // 设置干扰线的颜色 g.setColor(getRandColor(110, 120)); // 绘制干扰线 for (int i = 0; i <= lineNum; i++) { drowLine(g); } // 绘制随机字符 g.setFont(new Font(FONT_NAME, Font.ROMAN_BASELINE, FONT_SIZE)); for (int i = 1; i <= strNum; i++) { randomCode.append(drowString(g, i)); } g.dispose(); return image; } /** * 给定范围获得随机颜色 */ private Color getRandColor(int fc, int bc) { if (fc > 255) fc = 255; if (bc > 255) bc = 255; int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } /** * 绘制字符串 */ private String drowString(Graphics g, int i) { g.setColor(new Color(random.nextInt(101), random.nextInt(111), random .nextInt(121))); String rand = String.valueOf(getRandomString(random.nextInt(RANDOM_STRS .length()))); g.translate(random.nextInt(3), random.nextInt(3)); g.drawString(rand, 13 * i, 16); return rand; } /** * 绘制干扰线 */ private void drowLine(Graphics g) { int x = random.nextInt(width); int y = random.nextInt(height); int x0 = random.nextInt(16); int y0 = random.nextInt(16); g.drawLine(x, y, x + x0, y + y0); } /** * 获取随机的字符 */ private String getRandomString(int num) { return String.valueOf(RANDOM_STRS.charAt(num)); } public static void main(String[] args) { CaptchaUtil tool = new CaptchaUtil(); StringBuffer code = new StringBuffer(); BufferedImage image = tool.genRandomCodeImage(code); System.out.println(">>> random code =: " + code); try { // 将内存中的图片通过流动形式输出到客户端 ImageIO.write(image, "JPEG", new FileOutputStream(new File( "random-code.jpg"))); } catch (Exception e) { e.printStackTrace(); } } } |
[四]、创建验证码的servlet
CaptchaServlet.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package com.micmiu.modules.captcha; import java.awt.image.BufferedImage; import java.io.IOException; import javax.imageio.ImageIO; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * * @author <a href="http://www.micmiu.com">Michael Sun</a> */ public class CaptchaServlet extends HttpServlet { private static final long serialVersionUID = -124247581620199710L; public static final String KEY_CAPTCHA = "SE_KEY_MM_CODE"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置相应类型,告诉浏览器输出的内容为图片 resp.setContentType("image/jpeg"); // 不缓存此内容 resp.setHeader("Pragma", "No-cache"); resp.setHeader("Cache-Control", "no-cache"); resp.setDateHeader("Expire", 0); try { HttpSession session = req.getSession(); CaptchaUtil tool = new CaptchaUtil(); StringBuffer code = new StringBuffer(); BufferedImage image = tool.genRandomCodeImage(code); session.removeAttribute(KEY_CAPTCHA); session.setAttribute(KEY_CAPTCHA, code.toString()); // 将内存中的图片通过流动形式输出到客户端 ImageIO.write(image, "JPEG", resp.getOutputStream()); } catch (Exception e) { e.printStackTrace(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } } |
[五]、修改配置文件
在 web.xml 中增加配置:
1 2 3 4 5 6 7 8 9 |
<!-- captcha servlet config--> <servlet> <servlet-name>CaptchaServlet</servlet-name> <servlet-class>com.micmiu.modules.captcha.CaptchaServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CaptchaServlet</servlet-name> <url-pattern>/servlet/captchaCode</url-pattern> </servlet-mapping> |
修改 applicationContext-shiro.xml 中的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- Shiro Filter --> <bean id="myCaptchaFilter" class="com.micmiu.modules.support.shiro.FormAuthenticationCaptchaFilter"/> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login.do" /> <property name="successUrl" value="/index.do" /> <property name="filters"> <map> <entry key="authc" value-ref="myCaptchaFilter"/> </map> </property> <property name="filterChainDefinitions"> <value> /login.do = authc /logout.do = logout /servlet/* = anon /images/** = anon /js/** = anon /css/** = anon /** = user </value> </property> </bean> |
[六]、修改登录页面
login.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
<%@ page contentType="text/html;charset=UTF-8"%> <%@ page import="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"%> <%@ page import="org.apache.shiro.authc.ExcessiveAttemptsException"%> <%@ page import="org.apache.shiro.authc.IncorrectCredentialsException"%> <%@ include file="/include/taglibs.jsp"%> <html> <head> <title>登录页</title> </head> <body style="width: 99%"> <div> <c:choose> <c:when test="${shiroLoginFailure eq 'com.hx.web.excep.CaptchaException'}"> <div class="error-msg prepend-top">验证码错误,请重试.</div> </c:when> <c:when test="${shiroLoginFailure eq 'org.apache.shiro.authc.UnknownAccountException'}"> <div class="error-msg prepend-top">该用户不存在.</div> </c:when> <c:when test="${shiroLoginFailure eq 'org.apache.shiro.authc.IncorrectCredentialsException'}"> <div class="error-msg prepend-top">用户或密码错误.</div> </c:when> <c:when test="${shiroLoginFailure ne null}"> <div class="error-msg prepend-top">登录认证错误,请重试.</div> </c:when> </c:choose> <form:form id="loginForm" action="${ctx}/login.do" method="post"> <fieldset class="prepend-top"> <legend>系统登录</legend> <div class="field"> <label for="username" class="field">名称:</label> <input type="text" id="username" name="username" size="25" value="${username}" class="required" /> </div> <div class="field"> <label for="password" class="field">密码:</label> <input type="password" id="password" name="password" size="25" class="required" /> </div> <div class="field"> <label for="captcha" class="field">验证码:</label> <input type="text" id="captcha" name="captcha" size="4" maxlength="4" class="required" /> </div> <div class="field"> <label for="codeImg" class="field"></label> <img title="点击更换" id="img_captcha" onclick="javascript:refreshCaptcha();" src="servlet/captchaCode">(看不清<a href="javascript:void(0)" onclick="javascript:refreshCaptcha()">换一张</a>) </div> </fieldset> <div> <input type="checkbox" id="rememberMe" name="rememberMe" /> <label for="rememberMe">记住我</label> <span style="padding-left: 10px;"><input id="submit" class="button" type="submit" value="登录" /></span> </div> <div> (管理员<b>admin/admin</b>, 普通用户<b>user/user</b>) </div> </form:form> </div> </body> <script type="text/javascript"> $(document).ready(function() { $("#loginForm").validate(); }); var _captcha_id = "#img_captcha"; function refreshCaptcha() { $(_captcha_id).attr("src","servlet/captchaCode?t=" + Math.random()); } </script> </html> |
[七]、验证测试
启动项目后会看到如下页面:
本文介绍到此结束@Michael Sun.
原创文章,转载请注明: 转载自micmiu – 软件开发+生活点滴[ http://www.micmiu.com/ ]
本文链接地址: http://www.micmiu.com/opensource/security/shiro-web-captcha/
验证码的判断应该在过滤器而不是 Realm 之中,更合理些
servlet/captchaCode 这个路径怎么在项目中没有啊
这个就是servlet ,没有明白你说的意思
怎么感觉不对呀,我运行一直报无法转换org.apache.shiro.authc.UsernamePasswordToken cannot be cast to com.dounine.shiro.UsernamePasswordCaptchaToken
估计类继承错了
😛 整的太复杂了,重写认证类,login的时候,把验证码带过去。加一个验证码类!
复杂?最基本的实现
有帮助!谢谢! 🙂
能发一份demo么,最近正在研究这个,邮箱511098425@qq.com
https://github.com/micmiu/micmiu-web-sh/ 这个项目中有
楼主你能发一份源码吗?这两天正在研究这个东西。
UsernamePasswordCaptchaToken token = (UsernamePasswordCaptchaToken) authcToken;
为何我在这个地方就报UnknownAccountException异常了呢?
未知用户,估计是shiro的realm 实习有问题