接到一个需求,给旧项目加上验证码,其他项目也做过这个需求,但是这项目不一样,spingCloud我还没有接触过,这个项目用的不是传统的登录校验,而是用security来实现自动认证,任务时间要求紧,才疏学浅没办法在短时间内学会security,剑走偏锋先用自己的思路把功能实现,之后再改良,先把实现过程贴上来
网上找了一个生成验证码的工具
package com.renzheng.platform.baseCommon.utils.validate;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class ImageVerificationCodeUtils {
private int weight = 100; //验证码图片的长和宽
private int height = 40;
private String text; //用来保存验证码的文本内容
private Random r = new Random(); //获取随机数对象
//private String[] fontNames = {"宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312"}; //字体数组
//字体数组
private String[] fontNames = {"Georgia"};
//验证码数组
private String codes = "23456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
/**
* 获取随机的颜色
*
* @return
*/
private Color randomColor() {
int r = this.r.nextInt(225); //这里为什么是225,因为当r,g,b都为255时,即为白色,为了好辨认,需要颜色深一点。
int g = this.r.nextInt(225);
int b = this.r.nextInt(225);
return new Color(r, g, b); //返回一个随机颜色
}
/**
* 获取随机字体
*
* @return
*/
private Font randomFont() {
int index = r.nextInt(fontNames.length); //获取随机的字体
String fontName = fontNames[index];
int style = r.nextInt(4); //随机获取字体的样式,0是无样式,1是加粗,2是斜体,3是加粗加斜体
int size = r.nextInt(10) + 24; //随机获取字体的大小
return new Font(fontName, style, size); //返回一个随机的字体
}
/**
* 获取随机字符
*
* @return
*/
private char randomChar() {
int index = r.nextInt(codes.length());
return codes.charAt(index);
}
/**
* 画干扰线,验证码干扰线用来防止计算机解析图片
*
* @param image
*/
private void drawLine(BufferedImage image) {
int num = r.nextInt(10); //定义干扰线的数量
Graphics2D g = (Graphics2D) image.getGraphics();
for (int i = 0; i < num; i++) {
int x1 = r.nextInt(weight);
int y1 = r.nextInt(height);
int x2 = r.nextInt(weight);
int y2 = r.nextInt(height);
g.setColor(randomColor());
g.drawLine(x1, y1, x2, y2);
}
}
/**
* 创建图片的方法
*
* @return
*/
private BufferedImage createImage() {
//创建图片缓冲区
BufferedImage image = new BufferedImage(weight, height, BufferedImage.TYPE_INT_RGB);
//获取画笔
Graphics2D g = (Graphics2D) image.getGraphics();
//设置背景色随机
g.setColor(new Color(255, 255, r.nextInt(245) + 10));
g.fillRect(0, 0, weight, height);
//返回一个图片
return image;
}
/**
* 获取验证码图片的方法
*
* @return
*/
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g = (Graphics2D) image.getGraphics(); //获取画笔
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; i++) //画四个字符即可
{
String s = randomChar() + ""; //随机生成字符,因为只有画字符串的方法,没有画字符的方法,所以需要将字符变成字符串再画
sb.append(s); //添加到StringBuilder里面
float x = i * 1.0F * weight / 4; //定义字符的x坐标
g.setFont(randomFont()); //设置字体,随机
g.setColor(randomColor()); //设置颜色,随机
g.drawString(s, x, height - 5);
}
this.text = sb.toString();
drawLine(image);
return image;
}
/**
* 获取验证码文本的方法
*
* @return
*/
public String getText() {
return text;
}
public static void output(BufferedImage image, OutputStream out) throws IOException //将验证码图片写出的方法
{
ImageIO.write(image, "JPEG", out);
}
}
定义一个获取验证码的接口
public class VerifiCodeController {
@RequestMapping("/getVerifiCode")
@ResponseBody
public void getVerifiCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
/*
1.生成验证码
2.把验证码上的文本存在session中
3.把验证码图片发送给客户端
*/
ImageVerificationCodeUtils ivc = new ImageVerificationCodeUtils(); //用我们的验证码类,生成验证码类对象
BufferedImage image = ivc.getImage(); //获取验证码
request.getSession().setAttribute("rzVerificationCode", ivc.getText()); //将验证码的文本存在session中
ivc.output(image, response.getOutputStream());//将验证码图片响应给客户端
}
}
前端页面已经有了,只需要增加一个验证码的输入框跟图片展示,项目用的是jsp
<form id="login" method="post" action="${ctx}/login" style="margin:0 auto;">
<ul class="clearfix">
<li><input name="username" class="user" type="text" id="username" title="请输入帐号">
</li>
<li><input name="password" class="password" type="password" id="password" title="请输入密码">
</li>
<li style="width:106%">
<input name="verificationCode" type="text" id="verificationCode" style="width: 220px;height: 40px;float:left" title="请输入验证码"/>
<img id="yzm_img" title="点击刷新验证码" style="float:right" onclick="PageObject.getVerifiCode()" src="/verifiCode/getVerifiCode"/>
</li>
<li>
<a href="javascript:void(0)" onclick="PageObject.loginCheck();">登 录</a>
</li>
<input id="locking" type="hidden" value="${locking}" />
<input id="verificationCodeErr" type="hidden" value="${verificationCodeErr}" />
<input id="failCount" type="hidden" value="${FAIL_COUNT}" />
<input id="tipsValue" type="hidden" value="${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}" />
</ul>
<div id="tips" class="login-alert displayNone" >${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</div>
</form>
PageObject.getVerifiCode = function () {
$("#yzm_img").prop('src','/verifiCode/getVerifiCode?a='+new Date().getTime());
}
到目前为止,获取验证码接口已经写好,但是请求会被security拦截,所以找到继承WebSecurityConfigurerAdapter的security的配置文件,将/verifiCode路径放行
@Configuration
@ConditionalOnBean(value = {UserDetailsService.class,FilterInvocationSecurityMetadataSource.class})
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter{
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**")
.antMatchers("/favicon.ico")
.antMatchers("/common/**")
.antMatchers("/verifiCode/**");
}
}
接下来就是校验思路了,由于没有重写security的认证方法,我这里自定义了一个拦截器,在这个拦截器里面来做校验,拦截到login请求就执行判断,其他就放行
@Component
public class VerificationCodeFilter extends GenericFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if ("POST".equalsIgnoreCase(req.getMethod()) && "/login".equalsIgnoreCase(req.getServletPath())) {
Object failCount = req.getSession().getAttribute("FAIL_COUNT");//获取失败次数,为null则往下
if (failCount != null) {//获取配置的允许错误次数与锁定毫秒值
int setCount = Integer.valueOf(PropertiesConfigUtil.getProperties("lock_count"));
int setLockTime = Integer.valueOf(PropertiesConfigUtil.getProperties("lock_time"));
Integer count = Integer.valueOf(failCount.toString());//获取失败次数
if (count >= setCount ){
//失败次数大于允许失败次数,获取锁定时的毫秒
long lockTime = (long)req.getSession().getAttribute("LOCK_TIME");
long l = System.currentTimeMillis() - lockTime;
if (l < setLockTime){
//判断与锁定时所经过的毫秒差
req.getSession().setAttribute("locking",(setLockTime-l)/1000);
res.sendRedirect("login");
return;
} else {
req.getSession().removeAttribute("locking");
}
}
}
String verificationCode = req.getParameter("verificationCode");
String verificationCode1 = (String)req.getSession().getAttribute("rzVerificationCode");
if (verificationCode.equalsIgnoreCase(verificationCode1)){
chain.doFilter(req,res);
} else {
req.getSession().setAttribute("verificationCodeErr","验证码错误!");
res.sendRedirect("login");
}
} else {
chain.doFilter(req,res);
}
}
@Override
public void destroy() {
}
}
上面的接口里面加了个校验逻辑,登录错误次数大于n次,锁定n秒钟,n可配置到properties文件中,这个功能如果不需要可删除,如果需要,代码在下方,文件与读取工具类在下面
properties文件内容
lock_count=5
lock_time=600000
读取properties的工具类
public class PropertiesConfigUtil {
private static final Logger logger= LoggerFactory.getLogger(PropertiesConfigUtil.class);
private static Properties properties;
static {
loadProperties();
}
synchronized static private void loadProperties(){
logger.info("开始加载properties文件内容……");
properties=new Properties();
InputStream in=null;
try {
in= PropertiesConfigUtil.class.getClassLoader().getResourceAsStream("lock.properties");
properties.load(new BufferedReader(new InputStreamReader(in)));
} catch (IOException e){
logger.error(e.getMessage());
}finally {
if (in!=null){
try {
in.close();
}catch (IOException e){
logger.error("关闭文件流异常,异常信息为:"+e.getMessage());
}
}
}
logger.info("加载properties文件完成");
logger.info("properties文件内容为:"+properties);
}
public static String getProperties(String key){
if (properties==null){
loadProperties();
}
String result=properties.getProperty(key);
if (StringUtils.isNotBlank(result)){
return result;
}else {
return "";
}
}
public static String getProperties(String key,String defaultValue){
if (properties==null){
loadProperties();
}
return properties.getProperty(key,defaultValue);
}
}
登录失败需要记录失败的次数,实现AuthenticationFailureHandler接口,重写onAuthenticationFailure方法,
在方法中记录错误次数跟锁定的时间
@component
public class MySimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private String defaultFailureUrl = "/loginError";
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public MySimpleUrlAuthenticationFailureHandler() {
}
public MySimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
this.setDefaultFailureUrl(defaultFailureUrl);
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
this.logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(401, "Authentication Failed: " + exception.getMessage());
} else {
this.saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
} else {
this.logger.debug("Redirecting to " + this.defaultFailureUrl);
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
Object failCount = request.getSession().getAttribute("FAIL_COUNT");//获取错误次数
int i = 1;
if (failCount != null) {
i += Integer.valueOf(failCount.toString());//累加错误次数
}
request.getSession().setAttribute("FAIL_COUNT",i);//保存错误次数
int count = 5; //默认为5次
String lockCount = PropertiesConfigUtil.getProperties("lock_count");
if (lockCount != null) count = Integer.valueOf(lockCount);
if (i >= count) request.getSession().setAttribute("LOCK_TIME", System.currentTimeMillis());//错误次数多余五次,记录当前时间
if (this.forwardToDestination) {
request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
} else {
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
}
}
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), "'" + defaultFailureUrl + "' is not a valid redirect URL");
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return this.forwardToDestination;
}
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return this.redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
当然对应的,登录成功之后要把会话中的失败次数清空,实现AuthenticationSuccessHandler接口,自定成功后处理类
public class MySimpleUrlAuthenticationSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {
public MySimpleUrlAuthenticationSuccessHandler() {
}
public MySimpleUrlAuthenticationSuccessHandler(String defaultTargetUrl) {
this.setDefaultTargetUrl(defaultTargetUrl);
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
this.handle(request, response, authentication);
this.clearAuthenticationAttributes(request);
}
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute("SPRING_SECURITY_LAST_EXCEPTION");
session.removeAttribute("FAIL_COUNT");
}
}
}
@Component
public class MySavedRequestAwareAuthenticationSuccessHandler extends MySimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
public MySavedRequestAwareAuthenticationSuccessHandler() {
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
} else {
String targetUrlParameter = this.getTargetUrlParameter();
if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
this.logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
} else {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
在FormLoginWebSecurityConfigurerAdapter中配置这两个handler
@Configuration
@ConditionalOnBean(value = {UserDetailsService.class,FilterInvocationSecurityMetadataSource.class})
public static class FormLoginWebSecurityConfigurerAdapter extends BaseSecurityConfig {
private VerificationCodeFilter verificationCodeFilter;
private AuthenticationSuccessHandler mySavedRequestAwareAuthenticationSuccessHandler;
private AuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;
{
verificationCodeFilter = new VerificationCodeFilter();
mySavedRequestAwareAuthenticationSuccessHandler = new MySavedRequestAwareAuthenticationSuccessHandler();
mySimpleUrlAuthenticationFailureHandler = new MySimpleUrlAuthenticationFailureHandler();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**")
.antMatchers("/favicon.ico")
.antMatchers("/common/**")
.antMatchers("/verifiCode/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterBefore(verificationCodeFilter,UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().usernameParameter("username").passwordParameter("password").loginPage("/login").permitAll()
.defaultSuccessUrl("/index").failureUrl("/loginError")
.successHandler(mySavedRequestAwareAuthenticationSuccessHandler)//配置自定义登录成功处理器
.failureHandler(mySimpleUrlAuthenticationFailureHandler)//配置自定义登录成功处理器
.and()
.logout().logoutSuccessUrl("/login").invalidateHttpSession(true)
.and()
.exceptionHandling().accessDeniedPage("/deny")
.and()
.addFilterBefore(requestSecurityFilter(), FilterSecurityInterceptor.class);
}
}
}
最后加上完整的页面代码
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html id="thisIsLoginPage">
<head>
</head>
<body style="background:url(${ctx}/static/img/bg.jpg) no-repeat center;">
<%--<body>--%>
<div class="index_login">
<ol>
<li>管理平台</li>
</ol>
<form id="login" method="post" action="${ctx}/login" style="margin:0 auto;">
<ul class="clearfix">
<li><input name="username" class="user" type="text" id="username" title="请输入帐号">
</li>
<li><input name="password" class="password" type="password" id="password" title="请输入密码">
</li>
<li style="width:106%">
<input name="verificationCode" type="text" id="verificationCode" style="width: 220px;height: 40px;float:left" title="请输入验证码"/>
<img id="yzm_img" title="点击刷新验证码" style="float:right" onclick="PageObject.getVerifiCode()" src="/verifiCode/getVerifiCode"/>
</li>
<li>
<a href="javascript:void(0)" onclick="PageObject.loginCheck();">登 录</a>
</li>
<input id="locking" type="hidden" value="${locking}" />
<input id="verificationCodeErr" type="hidden" value="${verificationCodeErr}" />
<input id="failCount" type="hidden" value="${FAIL_COUNT}" />
<input id="tipsValue" type="hidden" value="${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}" />
</ul>
<div id="tips" class="login-alert displayNone" >${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</div>
</form>
<p class="footer">本系统建议分辨率在1024*768以上</p>
</div>
<script type="text/javascript" src="${ctx}/static/plugins/jquery-easyui/v1.4.4/jquery-3.4.0.min.js"></script>
<script type="text/javascript" src="${ctx}/static/js/common/FunctionPlugin.js"></script>
<script type="text/javascript" src="${ctx}/static/js/common/Utils.js"></script>
<script type="text/javascript">
(function($,window,PageObject) {
PageObject.colorValue = '';
PageObject.imgPosition = '';
// 页面初始化方法
PageObject.init = function(){
// 存在首页元素,直接刷新页面(防止页面嵌套)
if($('#centerLayout').length > 0){
window.location.reload();
}
// 判断是否存在错误提示,若存在,则显示提示框
if($('#tipsValue').val() != ''){
PageObject.showtips();
};
//验证码是否存在错误
if($('#verificationCodeErr').val() != ''){
$("#tips").html($('#verificationCodeErr').val())
PageObject.showtips();
<%session.removeAttribute("verificationCodeErr");%>
} else if($('#failCount').val() != ''){
//是否存在输入错误信息
if ($('#locking').val() != ''){
$("#tips").html("错误已到达"+ ${FAIL_COUNT} + "次, 请" + ${locking} +"秒后重试!")
}else {
$("#tips").html("错误"+ ${FAIL_COUNT} + "次")
}
PageObject.showtips();
<%--<%session.removeAttribute("verificationCodeErr");%>--%>
};
// 绑定输入框事件
this.dynamicEvent($("#username"),"user-focus","user-default");
this.dynamicEvent($("#password"),"password-focus","password-default");
var ua = navigator.userAgent.toLowerCase();//取得浏览器的版本信息等,转换为小写
if(ua.indexOf("msie") > -1){//判定为IE浏览器时
var safariVersion = ua.match(/msie ([\d.]+)/)[1];//取得版本号
if(safariVersion < 8){//如果浏览器低于IE8.0
var ieView = !-[1,]&&document.documentMode;
if (ieView<8){
$.messager.alert("提示","浏览器版本过低,如有系统使用需求请联系系统管理员协助升级浏览器!","info");
}
}
}
// 登录绑定enter键
Utils.keyEnter.on(0,PageObject.loginCheck);
};
// 登录校验及提交
PageObject.loginCheck = function(){
var usernameValue = $.trim($('#username').val());
var passwordValue = $.trim($('#password').val());
var verificationCodeValue = $.trim($('#verificationCode').val());
var tips=$("#tips");
if(usernameValue==""){
tips.html("请输入登录账号");
PageObject.showtips();
// 改变输入框样式
PageObject.setInputType($("#username"), "user-error");
return false;
}
else if(passwordValue==""){
tips.html("请输入登录密码");
PageObject.showtips();
// 改变输入框样式
PageObject.setInputType($("#password"), "password-error");
return false;
}
else if(verificationCodeValue==""){
tips.html("请输入验证码");
PageObject.showtips();
// 改变输入框样式
PageObject.setInputType($("#verificationCode"), "verificationCode-error");
return false;
}
$('#login').submit();
};
// 绑定输入框动态事件 focus、blur
PageObject.dynamicEvent = function($obj,focusClass,defaultClass){
$obj.focus(function(){
$obj.closest('li')[0].className = focusClass;
}).blur(function(){
$obj.closest('li')[0].className = defaultClass;
});
};
// 动态改变输入框样式
PageObject.setInputType = function($ele, classType){
$ele.closest('li')[0].className = classType;
};
// 显示提示信息
PageObject.showtips = function(){
$('.login-alert').fadeIn(1000,function(){
setTimeout(function(){
$('.login-alert').fadeOut(1000);
},5000);
});
};
PageObject.getVerifiCode = function () {
$("#yzm_img").prop('src','/verifiCode/getVerifiCode?a='+new Date().getTime());
}
})(jQuery,window,window.PageObject = {});
// 页面初始化
PageObject.init();
</script>
</body>
</html>
网友评论