美文网首页
spring security 添加登录前验证码校验的实现思路

spring security 添加登录前验证码校验的实现思路

作者: 小偏离 | 来源:发表于2020-05-21 17:16 被阅读0次

接到一个需求,给旧项目加上验证码,其他项目也做过这个需求,但是这项目不一样,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();">登&nbsp;&nbsp;录</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();">登&nbsp;&nbsp;录</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>

相关文章

网友评论

      本文标题:spring security 添加登录前验证码校验的实现思路

      本文链接:https://www.haomeiwen.com/subject/yyftahtx.html