Shiro在web应用中实现验证码、回显登录失败信息

转载:Shiro在web应用中实现验证码功能

目录结构:

  1. 概述
  2. 扩展shiro认证
  3. 验证码工具
  4. 验证码servlet
  5. 配置文件修改
  6. 修改登录页面
  7. 测试验证

[一]、概述

本文简单讲述在web应用整合shiro后,如何实现登录验证码认证的功能。

[二]、扩展shiro的认证

创建验证码异常类:CaptchaException.java

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

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

此过滤器继承FormAuthenticationFilter,FormAuthenticationFilter中在request范围类保存了用户登录失败时的错误信息:


由于在ShiroDbRealm(下文会提到)中登录认证时,如果验证失败,方法都是抛出:AuthenticationException 异常(父类异常),所以在页面获取request返回错误信息时,使用jstl无法区分异常类型,所以在新的FormAuthenticationCaptchaFilter中重写了保存异常信息到request范围的方法,使其返回准确的异常信息。

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);
 
	}
	
	/**
	 * 重写父类FormAuthenticationFilter的方法,返回登录验证异常对应真实异常子类信息
	 * 目的:用于登录失败时,在登陆页面返显错误信息,提示用户
	 */
	@Override
	protected void setFailureAttribute(ServletRequest request,
			AuthenticationException ae) {
		 String className ="";
		 if(ae instanceof LockedAccountException){
			 className=LockedAccountException.class.getName();
		 }else if(ae instanceof UnknownAccountException){
			 className=UnknownAccountException.class.getName();
		 }else if(ae instanceof CaptchaException){
			 className=CaptchaException.class.getName();
		 }else if(ae instanceof DisabledAccountException){
			 className=DisabledAccountException.class.getName();
		 }else if(ae instanceof IncorrectCredentialsException){
			 className=IncorrectCredentialsException.class.getName(); 
		 }else {
			 className = ae.getClass().getName(); 
		 }
	    request.setAttribute(getFailureKeyAttribute(), className);
		
	    
	   /* String className = ae.getClass().getName();
        request.setAttribute(getFailureKeyAttribute(), className);*/
	}
	
}



修改shiro认证逻辑: ShiroDbRealm.java

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:此工具类和servlet可以使用自己的定义

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

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 中增加配置:

<!-- 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 中的配置如下:

<!-- 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

<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"%>
<%@ 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"
						οnclick="javascript:refreshCaptcha();"
						src="servlet/captchaCode">(看不清<a href="javascript:void(0)" οnclick="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>


或者也可以这样获取异常信息,两种方式本质一样:

	<div >
				<%
					String error = (String) request
								.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
				%>
				<c:set var="exp_type" value="<%=error%>" />
				<c:set var="tips" value=""></c:set>
				<c:if test="${fn:contains(exp_type,'CaptchaException')}">
					<c:set var="tips" value="验证码错误,请确认后重新输入!"></c:set>
				</c:if>
				<c:if test="${fn:contains(exp_type,'UnknownAccountException')}">
					<c:set var="tips" value="该账号不存在,请确认后重新输入!"></c:set>
				</c:if>
				<c:if test="${fn:contains(exp_type,'DisabledAccountException')}">
					<c:set var="tips" value="该账号已被管理员限制,不允许登陆!"></c:set>
				</c:if>
				<c:if
					test="${fn:contains(exp_type,'IncorrectCredentialsException')}">
					<c:set var="tips" value="账号或密码错误,请确认后重新输入!"></c:set>
				</c:if>
				<c:if
					test="${fn:contains(exp_type,'LockedAccountException')}">
					<c:set var="tips" value="该账号已被锁定,请联系管理员!"></c:set>
				</c:if>
			</div>



[七]、验证测试

启动项目后会看到如下页面:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值