背景:
验证码是有效防止暴力破解的一种手段,常用做法是在服务端产生一串随机字符串与当前用户会话关联(我们通常说的放入 Session),然后向终端用户展现一张经过“扰乱”的图片,只有当用户输入的内容与服务端产生的内容相同时才允许进行下一步操作。
与其说在 shiro 中添加验证码,不如说是在 web 服务登录中添加验证码功能,因为这个功能和 shiro 完全没有任何关系,网上大都是实现 FormAuthenticationFilter 然后在 filter 中进行验证码校验,或者是在 shiroRealm 中进行验证码校验,一大堆的代码感觉很麻烦,下面进行最简单的代码实现验证码功能。直接在 login 方法内判断验证码是否正确。
验证码工具类:
创建生成验证码的工具类 CaptchaUtil ,代码如下所示:
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;
/**
* 验证码工具类
*/
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(
"/Desktop/ceshi.jpg")));
} catch (Exception e) {
e.printStackTrace();
}
}
}
验证码获取类:
创建获取验证码的 Controller 类 CaptchaController,代码如下所示:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.util.CaptchaUtil;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
@Controller
public class CaptchaController {
public static final String KEY_CAPTCHA = "KEY_CAPTCHA";
@RequestMapping("/verificationCode")
public void verificationCode(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
// 设置相应类型,告诉浏览器输出的内容为图片
response.setContentType("image/jpeg");
// 不缓存此内容
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expire", 0);
try {
HttpSession session = request.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", response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
}
配置 shiroConfig:
在 shiroConfig 中将获取验证码的功能开放权限,如下所示:
// Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
// .....省略
Map<String, String> map = new LinkedHashMap<>();
// 添加这块的过滤,否则请求进不来
map.put("/verificationCode", "anon");
// .....省略
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
修改 LoginController:
需要在 login() 方法中添加验证码校验,代码如下所示:
@Controller
@Slf4j
public class LoginController {
@Autowired
UserService userService;
@Autowired
RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher;
@GetMapping("/login")
public String login() {
return "login";
}
public static final String KEY_CAPTCHA = "KEY_CAPTCHA";
@PostMapping("/login")
@ResponseBody
public ResultData login(String userName,String password,String rememberMe,String captcha) {
ResultData resultData = new ResultData();
// 校验验证码
// session中的验证码
String sessionCaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KEY_CAPTCHA);
if (null == captcha || !captcha.equalsIgnoreCase(sessionCaptcha)) {
resultData.setCode(200);
resultData.setMessage("验证码错误!");
return resultData;
}
if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
resultData.setCode(200);
resultData.setMessage("用户名和密码不能为空!");
return resultData;
}
boolean rememberBoolean = false;
if("on".equals(rememberMe)) {
rememberBoolean = true;
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
userName,password,rememberBoolean);
try {
// 用户账号和密码进行验证
subject.login(usernamePasswordToken);
resultData.setCode(100);
return resultData;
} catch (Exception e) {
if(e instanceof UnknownAccountException) {
log.error("用户名不存在!", e);
resultData.setCode(200);
resultData.setMessage("用户名不存在!");
}
if(e instanceof IncorrectCredentialsException) {
log.error("用户名或者密码错误!", e);
resultData.setCode(200);
resultData.setMessage("用户名或者密码错误!");
}
if(e instanceof LockedAccountException) {
log.error("账号已被锁定,请联系管理员!", e);
resultData.setCode(200);
resultData.setMessage("账号已被锁定,请联系管理员!");
}
return resultData;
}
}
}
修改登录页面和 js :
需要在登录页面 login.jsp 中添加验证码,代码如下所示:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>一路发咨询网站</title>
</head>
<body>
<script type="text/javascript" src="/static/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="/static/js/login.js"></script>
<link rel="stylesheet" type="text/css" href="/static/css/login.css"/>
<h1>欢迎登录一路发咨询网站</h1>
<div>
<div id="father" style="background-image: url('/static/image/index.png');">
<div style="width:300px;height:100px;">
<div style="width:150px;float:left">
<span>用户名:<span></span>
</div>
<div style="width:150px;float:left;">
<input style="height:34px" type="text" id="userName" name="userName"/>
</div>
</div>
<div style="width:300px;height:100px;">
<div style="width:150px;float:left;">
<span>密码:<span></span>
</div>
<div style="width:150px;float:left;">
<input style="height:34px" type="password" id="password" name="password"/>
</div>
</div>
<div style="width:300px;height:100px;">
<div style="width:150px;float:left;">
<span>验证码:<span></span>
</div>
<div style="width:500px;float:left;border:1px solid black;font-size:23px">
<input style="height:34px" id="captcha" name="captcha"/>
<img id="captcha_img"/>
<a href="javascript:void(0)" onclick="javascript:refreshCaptcha()">换一张</a>
</div>
</div>
<div style="width:300px;height:100px;">
<div style="width:64px;float:left;margin-left:280px">
<input style="height:34px;width:34px;" type="checkbox" id="rememberMe" name="rememberMe" />
</div>
<div style="width:150px;float:left;margin-top:-4px">
<span>记住我<span></span>
</div>
</div>
<div style="margin-left:190px">
<input type="button" style="height:50px;width:90px;margin-top:5px;font-size:34px;font-weight:bold" onclick="login()" value="提交"/>
<input type="button" style="height:50px;margin-left:30px;width:150px;font-size:24px;font-weight:bold" onclick="unlock()" value="解锁用户"/>
</div>
</div>
</div>
</body>
</html>
修改登录页面所对应的 login.js ,代码如下所示:
function login(){
var userName = $("#userName").val();
var password = $("#password").val();
var rememberMe = $("#rememberMe").val();
var captcha = $("#captcha").val();
if (userName == '') {
alert('用户名不能为空!');
return;
}
if (password == '') {
alert('密码不能为空!');
return;
}
if (captcha == '') {
alert('验证码不能为空!');
return;
}
$.ajax({
url: 'login',
type: "POST",
async: false,
data: {"userName":userName, "password":password, "rememberMe":rememberMe,"captcha":captcha},
success: function (data) {
if (data.code == 100) {
window.location = 'shiro_index';
}else {
alert(data.message);
}
},
error:function(){
alert(data.message);
}
});
}
function unlock(){
var userName = $("#userName").val();
if (userName == '') {
alert('用户名不能为空!');
return;
}
$.ajax({
url: '/unlockAccount',
type: "POST",
async: false,
data: {"userName":userName},
success: function (data) {
if (data.code == 100) {
alert(data.message);
}else {
alert(data.message);
}
},
error:function(){
alert(data.message);
}
});
}
function refreshCaptcha(){
$("#captcha_img").attr("src","/verificationCode?id=" + new Date() + Math.floor(Math.random()*24));
}
$(function(){
refreshCaptcha();
});
测试:
启动工程,所显示的界面如下所示,输入验证码,可以正常的进入到后台进行校验。