您的位置:首页 > 大数据 > 人工智能

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

2017-08-15 10:18 656 查看
转载:Shiro在web应用中实现验证码功能

目录结构:

概述

扩展shiro认证

验证码工具

验证码servlet

配置文件修改

修改登录页面

测试验证

[一]、概述

本文简单讲述在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"
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>


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

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


[七]、验证测试

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



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息