目录结构:
概述
扩展shiro认证
验证码工具
验证码servlet
配置文件修改
修改登录页面
测试验证
[一]、概述
本文简单讲述在web应用整合shiro后,如何实现登录验证码认证的功能。
[二]、扩展shiro的认证
创建验证码异常类:CaptchaException.java
package com.micmiu.modules.support.shiro;
import org.apache.shiro.authc.AuthenticationException;
/**
*
* @author Michael Sun
*/
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 Michael Sun
*/
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 Michael Sun
*/
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 Michael Sun
*/
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 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;
}
/**
* 本函数输出将作为默认的输出.
*/
@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 Michael Sun
*/
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 Michael Sun
*/
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 中增加配置:
CaptchaServlet
com.micmiu.modules.captcha.CaptchaServlet
CaptchaServlet
/servlet/captchaCode
修改applicationContext-shiro.xml 中的配置如下:
/login.do = authc
/logout.do = logout
/servlet/* = anon
/images/** = anon
/js/** = anon
/css/** = anon
/** = user
[六]、修改登录页面
login.jsp
登录页test="${shiroLoginFailure eq 'com.hx.web.excep.CaptchaException'}">
test="${shiroLoginFailure eq 'org.apache.shiro.authc.UnknownAccountException'}">
test="${shiroLoginFailure eq 'org.apache.shiro.authc.IncorrectCredentialsException'}">
系统登录
名称:
id="username" name="username" size="25" value="${username}"
class="required" />
密码:
type="password" id="password" name="password" size="25"
class="required" />
验证码:
id="captcha" name="captcha" size="4" maxlength="4"
class="required" />
οnclick="javascript:refreshCaptcha();"
src="servlet/captchaCode">(看不清换一张)
for="rememberMe">记住我
id="submit" class="button" type="submit" value="登录" />
(管理员admin/admin, 普通用户user/user)
$(document).ready(function() {
$("#loginForm").validate();
});
var _captcha_id = "#img_captcha";
function refreshCaptcha() {
$(_captcha_id).attr("src","servlet/captchaCode?t=" + Math.random());
}
或者也可以这样获取异常信息,两种方式本质一样:
String error = (String) request
.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
%>
test="${fn:contains(exp_type,'IncorrectCredentialsException')}">
test="${fn:contains(exp_type,'LockedAccountException')}">
[七]、验证测试
启动项目后会看到如下页面: