前情提要:
因为公司对软件进行安全扫描,发现安全漏洞。需要将现有app软件的登录、注册功能添加图形验证码功能。验证码数据的生成以及校验需要在后台进行。
可参考后台管理系统的登录功能(如下图)。但是研究代码发现该功能的校验是在前台进行的,并且数据是后台生成后直接保存在页面隐藏框中。点击“登录”时进行校验。
后台代码如下:
package com.cpic.caf.hsp.web.controller.verifyCode;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.dcits.common.util.VerifyCodeImg;
@RequestMapping("/ui/verifyCode")
@Controller
public class VerifyCode {
/* 获取验证码图片 */
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/getVerifyCode")
@ResponseBody
public void getVerificationCode(HttpServletResponse response,
HttpServletRequest request,HttpSession session) {
try {
int width = 180;
int height = 45;
BufferedImage verifyImg = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 生成对应宽高的初始图片
String randomText = VerifyCodeImg.drawRandomText(width, height,
verifyImg);
// 单独的一个类方法,出于代码复用考虑,进行了封装。
// 功能是生成验证码字符并加上噪点,干扰线,返回值为验证码字符
request.getSession().setAttribute("verifyCode", randomText);
response.setContentType("image/png");// 必须设置响应内容类型为图片,否则前台不识别
OutputStream os = response.getOutputStream(); // 获取文件输出流
ImageIO.write(verifyImg, "png", os);// 输出图片流
os.flush();
os.close();// 关闭流
} catch (IOException e) {
this.logger.error(e.getMessage());
logger.info("异常信息:",e);
}
}
@RequestMapping("/checkVerifyCode")
@ResponseBody
public Map checkVerifyCode(HttpServletResponse response,
HttpServletRequest request,HttpSession session,String code) {
Map map = new HashMap();
String sessionCode = (String) request.getSession().getAttribute("verifyCode");
if (code != null && !"".equals(code) && sessionCode != null && !"".equals(sessionCode)) {
if (code.equalsIgnoreCase(sessionCode)) {
map.put("message", "success");
return map;
} else {
map.put("message", "fail");
return map;
}
} else {
map.put("message", "fail");
return map;
}
}
}
前台代码如下:
js如下:
$(function(){
var checkTrue = true;
function login(){
checkOut();
if(checkTrue == true){
var j_password = CryptoJS.SHA256($("#j_password").val()+$("#j_username").val());
$.ajax({
type:"POST",
dataType:"text",
url:"./j_spring_security_check",
data:"j_password="+j_password+"&j_username="+$('#j_username').val(),
success:function(data){
var da = $.parseJSON(data);
if(da.authentication== "true"){
sessionStorage.token = da.token;
window.top.location.href="./home";//home
}else{
$("#err").empty();
$("#_password").val("");
$("#err").append("用户名或密码错误!");
}
},
error:function(data){
alert("失败");
}
});
}
}
$("#j_login").click(function(){
var code = $("input[name='verifyInput']").val();
var inputCode = document.getElementById("inputCode").value;
var callerKeyId = sessionStorage.callerKeyId;
var sessionId = sessionStorage.sessionId;
var applicationId = sessionStorage.applicationId;
if(!!callerKeyId && !!sessionId && !!applicationId ){
var j_password = CryptoJS.SHA256($("#j_password").val()+$("#j_username").val());
$.ajax({
type:"POST",
dataType:"text",
url:"./j_spring_security_check",
data:"j_password="+j_password+"&j_username="+$('#j_username').val()+"&verifyCode="+inputCode,
success:function(data){
var da = $.parseJSON(data);
if(da.authentication== "true"){
sessionStorage.token = da.token;
window.top.location.href="./home";//home
}else{
alert('登录失败,请联系管理员添加此用户');
closeWindow();
}
},
error:function(data){
alert('打开系统失败,系统错误');
closeWindow();
}
});
}else{
$("#err").empty();
if (inputCode.length <= 0) {
// layer.alert('请输入验证码!', {closeBtn: 0});
$("#err").append("请输入验证码!");
}else {
if(checkTrue == true){
var j_password = CryptoJS.SHA256($("#j_password").val()+$("#j_username").val());
$.ajax({
type:"POST",
dataType:"text",
url:"./j_spring_security_check",
data:"j_password="+j_password+"&j_username="+$('#j_username').val()+"&verifyCode="+inputCode,
success:function(data){
var da = $.parseJSON(data);
if(da.authentication== "true"){
sessionStorage.token = da.token;
window.top.location.href="./home";//home
}else{
$("#err").empty();
changeCode();
$("#inputCode").val('');
if(da.authentication== "validCodeFalse"){
$("#err").append("验证码错误!");
}else if(da.authentication== "loginInfoFalse"){
$("#_password").val("");
$("#err").append("用户名或密码错误!");
}
}
},
error:function(data){
alert("失败");
}
});
}
}
}
});
$("body").keydown(function(event) {
if (event.keyCode == "13" || event.which == 13) {//keyCode=13是回车键
$("#j_login").click();
}
});
function checkOut() {
if($("#j_password").val()==""||$("#j_username").val()==""){
$("#err").empty();
$("#err").append("用户名或密码为空!");
checkTrue = false;
}else{
checkTrue = true;
}
}
$("#resetButton").click(function(){
$("#j_username").val("");
$("#j_password").val("");
$("#inputCode").val("");
$("#err").empty();
});
});
因此,这个后台管理系统的登录功能不太适用于app的登录、注册需求。
思考:
1、如果使用redis保存验证码数据,那么key值存什么?
我自己想到的是保存客户端的用户ip作为key值。
2、如果使用redis保存验证码数据,那么如何删除已经使用过或者已经过期的数据?
redis自带删除功能,定时删除、惰性删除、定期删除。
3、redis的使用,以及如何验证数据。
redis有客户端。
redis的数据是保存在内存中。(其实并没有很理解是哪里。)
改良之后的代码:
package com.cpic.caf.hsp.app.controller.verifyCode;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.jfree.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cpic.caf.hsp.utils.redis.RedisUtil;
import com.dcits.common.util.VerifyCodeImg;
@RequestMapping("/ui/verifyCode")
@Controller
public class VerifyCode {
/* 获取验证码图片 */
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/getVerifyCode")
@ResponseBody
public void getVerificationCode(HttpServletResponse response,
HttpServletRequest request,HttpSession session) {
try {
int width = 180;
int height = 45;
BufferedImage verifyImg = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 生成对应宽高的初始图片
String randomText = VerifyCodeImg.drawRandomText(width, height,
verifyImg);
// 单独的一个类方法,出于代码复用考虑,进行了封装。
// 功能是生成验证码字符并加上噪点,干扰线,返回值为验证码字符
//request.getSession().setAttribute("verifyCode", randomText);
String ip = request.getRemoteAddr();
redisUtil.setOp(ip+"_VerifyCode", randomText);
Log.info("ip_VerifyCode-----"+ip+"_"+randomText);
redisUtil.setEx(ip+"_VerifyCode", 60, randomText);
response.setContentType("image/png");// 必须设置响应内容类型为图片,否则前台不识别
OutputStream os = response.getOutputStream(); // 获取文件输出流
ImageIO.write(verifyImg, "png", os);// 输出图片流
os.flush();
os.close();// 关闭流
} catch (IOException e) {
this.logger.error(e.getMessage());
logger.info("异常信息:",e);
}
}
@RequestMapping("/checkVerifyCode")
@ResponseBody
public Map checkVerifyCode(HttpServletResponse response,
HttpServletRequest request,HttpSession session,String code) {
Map map = new HashMap();
//String sessionCode = (String) request.getSession().getAttribute("verifyCode");
//从redis中获取
String ip = request.getRemoteAddr();
String redisCode = redisUtil.getOp(ip+"_VerifyCode");
if (code != null && !"".equals(code) && redisCode != null && !"".equals(redisCode)) {
if (code.equalsIgnoreCase(redisCode)) {
map.put("message", "success");
return map;
} else {
map.put("message", "fail");
return map;
}
} else {
map.put("message", "fail");
return map;
}
}
}