Spring Security 图片验证码功能的实例代码
验证码逻辑
以前在项目中也做过验证码,生成验证码的代码网上有很多,也有一些第三方的jar包也可以生成漂亮的验证码。验证码逻辑很简单,就是在登录页放一个image标签,src指向一个controller,这个controller返回把生成的图片以输出流返回给页面,生成图片的同时把图片上的文本放在session,登录的时候带过来输入的验证码,从session中取出,两者对比。这位老师讲的用spring security集成验证码,大体思路和我说的一样,但更加规范和通用些。
spring security是一系列的过滤器链,所以在这里验证码也声明为过滤器,加在过滤器链的 登录过滤器之前,然后自定义一个异常类,来响应验证码的错误信息。
代码结构:
验证码代码放在core项目,在browser项目做一下配置。
主要代码:
1,imagecode:
首先是imagecode类,封装验证码图片、文本、过期时间
package com.imooc.security.core.validate.code; import java.awt.image.bufferedimage; import java.time.localdatetime; import java.time.localtime; /** * 验证码 * classname: imagecode * @description: 验证码 * @author lihaoyang * @date 2018年3月1日 */ public class imagecode { private bufferedimage image; private string code; private localdatetime expiretime;//过期时间点 /** * * <p>description: </p> * @param image * @param code * @param expiretn 多少秒过期 */ public imagecode(bufferedimage image, string code, int expiretn) { super(); this.image = image; this.code = code; //过期时间=当前时间+过期秒数 this.expiretime = localdatetime.now().plusseconds(expiretn); } public imagecode(bufferedimage image, string code, localdatetime expiretime) { super(); this.image = image; this.code = code; this.expiretime = expiretime; } /** * 验证码是否过期 * @description: 验证码是否过期 * @param @return true 过期,false 没过期 * @return boolean true 过期,false 没过期 * @throws * @author lihaoyang * @date 2018年3月2日 */ public boolean isexpired(){ return localdatetime.now().isafter(expiretime); } public bufferedimage getimage() { return image; } public void setimage(bufferedimage image) { this.image = image; } public string getcode() { return code; } public void setcode(string code) { this.code = code; } public localdatetime getexpiretime() { return expiretime; } public void setexpiretime(localdatetime expiretime) { this.expiretime = expiretime; } }
verifycode:生成验证码的工具类,在这里 当然也可以使用第三方jar包,无所谓。
validatecodeexception:封装验证码异常
/** * @title: validatecodeexception.java * @package com.imooc.security.core.validate.code * @description: todo * @author lihaoyang * @date 2018年3月2日 */ package com.imooc.security.core.validate.code; import org.springframework.security.core.authenticationexception; /** * classname: validatecodeexception * @description: 验证码错误异常,继承spring security的认证异常 * @author lihaoyang * @date 2018年3月2日 */ public class validatecodeexception extends authenticationexception { /** * @fields serialversionuid : todo */ private static final long serialversionuid = 1l; public validatecodeexception(string msg) { super(msg); } }
validatecodefilter:验证码过滤器
逻辑:继承onceperrequestfilter 保证过滤器每次只会被调用一次(不太清楚为什么),注入认证失败处理器,在验证失败时调用。
package com.imooc.security.core.validate.code; import java.io.ioexception; import javax.servlet.filterchain; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.apache.commons.lang.stringutils; import org.springframework.security.web.authentication.authenticationfailurehandler; import org.springframework.social.connect.web.httpsessionsessionstrategy; import org.springframework.social.connect.web.sessionstrategy; import org.springframework.web.bind.servletrequestbindingexception; import org.springframework.web.bind.servletrequestutils; import org.springframework.web.context.request.servletwebrequest; import org.springframework.web.filter.onceperrequestfilter; /** * 处理登录验证码过滤器 * classname: validatecodefilter * @description: * onceperrequestfilter:spring提供的工具,保证过滤器每次只会被调用一次 * @author lihaoyang * @date 2018年3月2日 */ public class validatecodefilter extends onceperrequestfilter{ //认证失败处理器 private authenticationfailurehandler authenticationfailurehandler; //获取session工具类 private sessionstrategy sessionstrategy = new httpsessionsessionstrategy(); @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { //如果是 登录请求 则执行 if(stringutils.equals("/authentication/form", request.getrequesturi()) &&stringutils.equalsignorecase(request.getmethod(), "post")){ try { validate(new servletwebrequest(request)); } catch (validatecodeexception e) { //调用错误处理器,最终调用自己的 authenticationfailurehandler.onauthenticationfailure(request, response, e); return ;//结束方法,不再调用过滤器链 } } //不是登录请求,调用其它过滤器链 filterchain.dofilter(request, response); } /** * 校验验证码 * @description: 校验验证码 * @param @param request * @param @throws servletrequestbindingexception * @return void * @throws validatecodeexception * @author lihaoyang * @date 2018年3月2日 */ private void validate(servletwebrequest request) throws servletrequestbindingexception { //拿出session中的imagecode对象 imagecode imagecodeinsession = (imagecode) sessionstrategy.getattribute(request, validatecodecontroller.session_key); //拿出请求中的验证码 string imagecodeinrequest = servletrequestutils.getstringparameter(request.getrequest(), "imagecode"); //校验 if(stringutils.isblank(imagecodeinrequest)){ throw new validatecodeexception("验证码不能为空"); } if(imagecodeinsession == null){ throw new validatecodeexception("验证码不存在,请刷新验证码"); } if(imagecodeinsession.isexpired()){ //从session移除过期的验证码 sessionstrategy.removeattribute(request, validatecodecontroller.session_key); throw new validatecodeexception("验证码已过期,请刷新验证码"); } if(!stringutils.equalsignorecase(imagecodeinsession.getcode(), imagecodeinrequest)){ throw new validatecodeexception("验证码错误"); } //验证通过,移除session中验证码 sessionstrategy.removeattribute(request, validatecodecontroller.session_key); } public authenticationfailurehandler getauthenticationfailurehandler() { return authenticationfailurehandler; } public void setauthenticationfailurehandler(authenticationfailurehandler authenticationfailurehandler) { this.authenticationfailurehandler = authenticationfailurehandler; } }
validatecodecontroller:生成验证码control
package com.imooc.security.core.validate.code; import java.io.ioexception; import javax.imageio.imageio; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.social.connect.web.httpsessionsessionstrategy; import org.springframework.social.connect.web.sessionstrategy; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.restcontroller; import org.springframework.web.context.request.servletwebrequest; /** * 验证码control * classname: validatecodecontroller * @description: todo * @author lihaoyang * @date 2018年3月1日 */ @restcontroller public class validatecodecontroller { public static final string session_key = "session_key_image_code"; //获取session private sessionstrategy sessionstrategy = new httpsessionsessionstrategy(); @getmapping("/verifycode/image") public void createcode(httpservletrequest request,httpservletresponse response) throws ioexception{ imagecode imagecode = createimagecode(request, response); sessionstrategy.setattribute(new servletwebrequest(request), session_key, imagecode); imageio.write(imagecode.getimage(), "jpeg", response.getoutputstream()); } private imagecode createimagecode(httpservletrequest request, httpservletresponse response) { verifycode verifycode = new verifycode(); return new imagecode(verifycode.getimage(),verifycode.gettext(),60); } }
browsersecurityconfig里进行过滤器配置:
package com.imooc.security.browser; import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.security.config.annotation.web.builders.httpsecurity; import org.springframework.security.config.annotation.web.configuration.websecurityconfigureradapter; import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder; import org.springframework.security.crypto.password.passwordencoder; import org.springframework.security.web.authentication.authenticationfailurehandler; import org.springframework.security.web.authentication.authenticationsuccesshandler; import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter; import com.imooc.security.core.properties.securityproperties; import com.imooc.security.core.validate.code.validatecodefilter; @configuration //这是一个配置 public class browsersecurityconfig extends websecurityconfigureradapter{ //读取用户配置的登录页配置 @autowired private securityproperties securityproperties; //自定义的登录成功后的处理器 @autowired private authenticationsuccesshandler imoocauthenticationsuccesshandler; //自定义的认证失败后的处理器 @autowired private authenticationfailurehandler imoocauthenticationfailurehandler; //注意是org.springframework.security.crypto.password.passwordencoder @bean public passwordencoder passwordencoder(){ //bcryptpasswordencoder implements passwordencoder return new bcryptpasswordencoder(); } //版本二:可配置的登录页 @override protected void configure(httpsecurity http) throws exception { //验证码过滤器 validatecodefilter validatecodefilter = new validatecodefilter(); //验证码过滤器中使用自己的错误处理 validatecodefilter.setauthenticationfailurehandler(imoocauthenticationfailurehandler); //实现需要认证的接口跳转表单登录,安全=认证+授权 //http.httpbasic() //这个就是默认的弹框认证 // http.addfilterbefore(validatecodefilter, usernamepasswordauthenticationfilter.class)//把验证码过滤器加载登录过滤器前边 .formlogin() //表单认证 .loginpage("/authentication/require") //处理用户认证browsersecuritycontroller //登录过滤器usernamepasswordauthenticationfilter默认登录的url是"/login",在这能改 .loginprocessingurl("/authentication/form") .successhandler(imoocauthenticationsuccesshandler)//自定义的认证后处理器 .failurehandler(imoocauthenticationfailurehandler) //登录失败后的处理 .and() .authorizerequests() //下边的都是授权的配置 // /authentication/require:处理登录,securityproperties.getbrowser().getloginpage():用户配置的登录页 .antmatchers("/authentication/require", securityproperties.getbrowser().getloginpage(),//放过登录页不过滤,否则报错 "/verifycode/image").permitall() //验证码 .anyrequest() //任何请求 .authenticated() //都需要身份认证 .and() .csrf().disable() //关闭csrf防护 ; } }
登陆页:登陆页做的比较粗糙,其实验证码可以在验证码input失去焦点的时候做校验,还可以做个点击图片刷新验证码功能,这里就不做了。
<body> demo 登录页. <br> <form action="/authentication/form" method="post"> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"/></td> <td></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"/></td> <td></td> </tr> <tr> <td>验证码:</td> <td> <input width="100" type="text" name="imagecode"/> </td> <td> <img src="/verifycode/image"/> </td> </tr> <tr> <td colspan="2" align="right"><button type="submit">登录</button></td> </tr> </table> </form> </body>
访问 :
响应自定义的异常信息
大体功能已经没问题了。但是不够通用,比如验证码图片的宽高、过期时间、过滤的url、验证码成逻辑都是写死的。这些可以做成活的,现在把验证码做成一个过滤器的好处体现出来了。我们可以配置需要过滤的url,有时候可能不只是登陆页需要验证码,这样更加通用。
1,通用性改造 之 验证码基本参数可配
做成可配置的,那个应用引用该模块,他自己配置去,不配置就使用默认配置。而且,配置既可以在请求url中声明,也可以在应用中声明,老师的确是老师,代码通用性真好!
想要实现的效果是,在application.properties里做这样的配置:
#验证码 图片宽、高、字符个数 imooc.security.code.image.width = 100 imooc.security.code.image.height = 30 imooc.security.code.image.length = 6
然后就能控制验证码的效果,因为验证码还分图片验证码、短信验证码,所以多做了一级.code.image,这就用到了springboot的自定义配置文件,需要声明对应的java类:
需要在securityproperties里声明code属性:
package com.imooc.security.core.properties; import org.springframework.boot.context.properties.configurationproperties; import org.springframework.context.annotation.configuration; /** * 自定义配置项 * classname: securityproperties * @description: 自定义配置项 * 这个类会读取application.properties里所有以imooc.security开头的配置项 * * imooc.security.browser.loginpage = /demo-login.html * 其中的browser的配置会读取到browserproperties中去 * 这是以点分割的,一级一级的和类的属性对应 * @author lihaoyang * @date 2018年2月28日 */ @configurationproperties(prefix="imooc.security") public class securityproperties { private browserproperties browser = new browserproperties(); private validatecodeproperties code = new validatecodeproperties(); public browserproperties getbrowser() { return browser; } public void setbrowser(browserproperties browser) { this.browser = browser; } public validatecodeproperties getcode() { return code; } public void setcode(validatecodeproperties code) { this.code = code; } }
validatecodeproperties:
package com.imooc.security.core.properties; /** * 验证码配置 * classname: validatecodeproperties * @description: 验证码配置,验证码有图片验证码、短信验证码等,所以再包一层 * @author lihaoyang * @date 2018年3月2日 */ public class validatecodeproperties { //默认配置 private imagecodeproperties image = new imagecodeproperties(); public imagecodeproperties getimage() { return image; } public void setimage(imagecodeproperties image) { this.image = image; } }
imagecodeproperties:
package com.imooc.security.core.properties; /** * 图片验证码配置类 * classname: imagecodeproperties * @description: 图片验证码配置类 * @author lihaoyang * @date 2018年3月2日 */ public class imagecodeproperties { //图片宽 private int width = 67; //图片高 private int height = 23; //验证码字符个数 private int length = 4; //过期时间 private int expirein = 60; public int getwidth() { return width; } public void setwidth(int width) { this.width = width; } public int getheight() { return height; } public void setheight(int height) { this.height = height; } public int getlength() { return length; } public void setlength(int length) { this.length = length; } public int getexpirein() { return expirein; } public void setexpirein(int expirein) { this.expirein = expirein; } }
请求级的配置,如果请求里带的有验证码的参数,就用请求里的:
在validatecodecontroller的createimagecode方法做控制,判断请求参数是否有这些参数,有的话,传给验证码生成类verifycode,在生成的时候就能动态控制了。
private imagecode createimagecode(httpservletrequest request, httpservletresponse response) { //先从request里读取有没有长、宽、字符个数参数,有的话就用,没有用默认的 int width = servletrequestutils.getintparameter(request, "width",securityproperties.getcode().getimage().getwidth()); int height = servletrequestutils.getintparameter(request, "height",securityproperties.getcode().getimage().getheight()); int charlength = this.securityproperties.getcode().getimage().getlength(); verifycode verifycode = new verifycode(width,height,charlength); return new imagecode(verifycode.getimage(),verifycode.gettext(),this.securityproperties.getcode().getimage().getexpirein()); }
verifycode:
public verifycode(int w, int h, int charlength) { super(); this.w = w; this.h = h; this.charlength = charlength; }
实验:在demo项目做应用级配置
登录表单做请求级配置
<img src="/verifycode/image?width=200"/>
访问:
长度为请求级带的参数200,高为30,字符为配置的6个。
2,通用性改造 之 验证码拦截的接口可配置
先要的效果就是再application.properties
里能动态配置需要拦截的接口:
imagecodeproperties
新增一个属性:private string url
; //拦截的url,来匹配上图的配置。
核心,验证码过滤器需要修改:
1,在拦截器里声明一个set集合,用来存储配置文件里配置的需要拦截的urls。
2,实现initializingbean接口,目的: 在其他参数都组装完毕的时候,初始化需要拦截的urls的值,重写afterpropertiesset方法来实现。
3,注入securityproperties,读取配置文件
4,实例化antpathmatcher工具类,这是一个匹配器
5,在browser项目的browsersecurityconfig里设置调用一下afterpropertiesset方法。
6,在引用该模块的demo项目的application.properties里配置要过滤的url
validatecodefilter:
/** * 处理登录验证码过滤器 * classname: validatecodefilter * @description: * 继承onceperrequestfilter:spring提供的工具,保证过滤器每次只会被调用一次 * 实现 initializingbean接口的目的: * 在其他参数都组装完毕的时候,初始化需要拦截的urls的值 * @author lihaoyang * @date 2018年3月2日 */ public class validatecodefilter extends onceperrequestfilter implements initializingbean{ //认证失败处理器 private authenticationfailurehandler authenticationfailurehandler; //获取session工具类 private sessionstrategy sessionstrategy = new httpsessionsessionstrategy(); //需要拦截的url集合 private set<string> urls = new hashset<>(); //读取配置 private securityproperties securityproperties; //spring工具类 private antpathmatcher antpathmatcher = new antpathmatcher(); @override public void afterpropertiesset() throws servletexception { super.afterpropertiesset(); //读取配置的拦截的urls string[] configurls = stringutils.splitbywholeseparatorpreservealltokens(securityproperties.getcode().getimage().geturl(), ","); for (string configurl : configurls) { urls.add(configurl); } //登录的请求一定拦截 urls.add("/authentication/form"); } @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { /** * 可配置的验证码校验 * 判断请求的url和配置的是否有匹配的,匹配上了就过滤 */ boolean action = false; for(string url:urls){ if(antpathmatcher.match(url, request.getrequesturi())){ action = true; } } if(action){ try { validate(new servletwebrequest(request)); } catch (validatecodeexception e) { //调用错误处理器,最终调用自己的 authenticationfailurehandler.onauthenticationfailure(request, response, e); return ;//结束方法,不再调用过滤器链 } } //不是登录请求,调用其它过滤器链 filterchain.dofilter(request, response); } //省略无关代码,,, }
browsersecurityconfig:
配置url:
#验证码拦截的接口配置 imooc.security.code.image.url = /user,/user/*
测试:/user /user/1 被拦截了
访问登录页,不写验证码:
和预期一致。至此,动态配置拦截接口完成
3,验证码的生成逻辑可配置
写的比较好的程序,一般都开放接口,可以让用户去自定义实现,如果不实现就用默认的实现,下面来做这件事,使验证码的生成可以自己实现。如果要想把验证码的生成逻辑做成可配置的,就不能只写一个图片验证码生成器的类了,需要把验证码生成提取成一个接口validatecodegenerator,一个生成验证码的方法generator()。因为验证码还有图片验证码、短信验证码等,这样,我们在自己的验证模块里做一个默认的实现,如图片验证码的实现imagecodegenerator,在imagecodegenerator里我们不在该类上加@component注解。然后使用写一个验证码bean的配置类validatecodebeanconfig,这个配置类配置各种需要的验证码实现类bean如图片验证码实现imagecodegenerator、短信验证码等,他们返回类型都是validatecodegenerator,使用@conditionalonmissingbean(name="imagecodegenerator")注解,可以判断如果当前spring容器有名字为imagecodegenerator的bean时,就使用,没有的话再配置,这样如果别人引用了你的该模块,如果别人自己实现了验证码生成validatecodegenerator接口,他们配置了实现类的name为imagecodegenerator,就用他们自己的实现,这样就做到了程序的可扩展性。
主要代码:
代码生成器接口validatecodegenerator:
package com.imooc.security.core.validate.code; import org.springframework.web.context.request.servletwebrequest; /** * 验证码生成接口 * classname: validatecodegenerator * @description: todo * @author lihaoyang * @date 2018年3月2日 */ public interface validatecodegenerator { /** * 图片验证码生成接口 * @description: todo * @param @param request * @param @return * @return imagecode * @throws * @author lihaoyang * @date 2018年3月2日 */ imagecode generator(servletwebrequest request); }
图片验证码生成器实现imagecodegenerator:
package com.imooc.security.core.validate.code; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; import org.springframework.web.bind.servletrequestutils; import org.springframework.web.context.request.servletwebrequest; import com.imooc.security.core.properties.securityproperties; /** * 图片验证码生成类 * classname: imagecodegenerator * @description: todo * @author lihaoyang * @date 2018年3月2日 */ public class imagecodegenerator implements validatecodegenerator { @autowired private securityproperties securityproperties; @override public imagecode generator(servletwebrequest request) { //先从request里读取有没有长、宽、字符个数参数,有的话就用,没有用默认的 int width = servletrequestutils.getintparameter(request.getrequest(), "width",securityproperties.getcode().getimage().getwidth()); int height = servletrequestutils.getintparameter(request.getrequest(), "height",securityproperties.getcode().getimage().getheight()); int charlength = this.securityproperties.getcode().getimage().getlength(); verifycode verifycode = new verifycode(width,height,charlength); return new imagecode(verifycode.getimage(),verifycode.gettext(),this.securityproperties.getcode().getimage().getexpirein()); } public securityproperties getsecurityproperties() { return securityproperties; } public void setsecurityproperties(securityproperties securityproperties) { this.securityproperties = securityproperties; } }
validatecodebeanconfig:
package com.imooc.security.core.validate.code; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.autoconfigure.condition.conditionalonmissingbean; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import com.imooc.security.core.properties.securityproperties; /** * 配置验证码生成接口validatecodegenerator的实际实现类的bean * classname: validatecodebeanconfig * @description: * 配置验证码生成接口validatecodegenerator的实际实现类的bean * 如图片验证码的实现、短信验证码的实现 * @author lihaoyang * @date 2018年3月5日 */ @configuration public class validatecodebeanconfig { @autowired private securityproperties securityproperties; /** * @description: * @conditionalonmissingbean注解意思是当spring容器不存在imagecodegenerator时才给配置一个该bean * 作用是使程序更具可扩展性,该配置类是配置在core模块,这就意味着,如果引用该模块的项目 * 如果有一个自己的实现,实现了validatecodegenerator接口,定义了自己的实现,名字也叫imagecodegenerator时, * 就用应用级别的实现,没有的话就用这个默认实现。 * @param @return * @return validatecodegenerator * @throws * @author lihaoyang * @date 2018年3月5日 */ @bean @conditionalonmissingbean(name="imagecodegenerator") public validatecodegenerator imagecodegenerator(){ imagecodegenerator codegenerator = new imagecodegenerator(); codegenerator.setsecurityproperties(securityproperties); return codegenerator; } }
这样,如果哪个模块引用了这个验证码模块,他自定义了实现,如:
package com.imooc.code; import org.springframework.stereotype.component; import org.springframework.web.context.request.servletwebrequest; import com.imooc.security.core.validate.code.imagecode; import com.imooc.security.core.validate.code.validatecodegenerator; @component("imagecodegenerator") public class demoimagecodegenerator implements validatecodegenerator { @override public imagecode generator(servletwebrequest request) { system.err.println("demo项目实现的生成验证码,,,"); return null; } }
这样validatecodebeanconfig在配置验证码bean时,就会使用使用者自定义的实现。
完整代码放在了github:
总结
以上所述是小编给大家介绍的spring security 图片验证码功能的实例代码,希望对大家有所帮助
上一篇: 套路层出不穷的年代,如何防被割韭菜
下一篇: Java上转型和下转型对象
推荐阅读
-
Vue press 支持图片放大功能的实例代码
-
Spring boot的上传图片功能实例详解
-
ionic4+angular7+cordova上传图片功能的实例代码
-
spring boot实现自动输出word文档功能的实例代码
-
Spring Security 图片验证码功能的实例代码
-
Android 使用fast-verification实现验证码填写功能的实例代码
-
Vue press 支持图片放大功能的实例代码
-
分享利用node-images实现图片服务器功能的实例代码
-
spring通过kaptcha配置验证码生成的代码实例
-
HTML5 Plus 实现手机APP拍照或相册选择图片上传功能的实例代码