Spring 全家桶之 Spring Security(五)
作者:小白
- 2022 年 8 月 23 日 上海
本文字数:5562 字
阅读完需:约 18 分钟
一、自定义验证码
在 Controller 包中创建 CaptchaController,用于生成验证码图像,返回验证码图片,并保存图片中的验证码在 Session 中,方便登录时校验验证码
@Controller
@RequestMapping("/captcha")
public class CaptchaController {
// 定义一个值,用来生成验证码的图片
// 宽度
private int width = 120;
// 高度
private int height = 30;
// 图片内容在图片的起始位置
private int drawY = 22;
// 文字的间隔
private int space = 22;
// 验证码有几个文字
private int charCount = 4;
// 验证码内容的数组
private String[] chars = {"A", "B", "C", "D", "E", "F", "G", "0", "1", "2", "3", "4", "5", "6", "8"};
// 生成验证码内容,在一个图片上写入文字
@GetMapping("/create")
public void createCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 需要在内存中绘制图片,向图片中写入文字,将绘制好的图片响应给请求
// 创建一个背景透明的图片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取画笔
Graphics graphics = image.getGraphics();
// 设置背景色
graphics.setColor(Color.white);
// 给画板涂成白色
graphics.fillRect(0, 0, width, height);
// 画内容
// 创建一个字体
Font font = new Font("宋体",Font.BOLD,18);
graphics.setFont(font);
graphics.setColor(Color.black);
// 在画布上写字
// graphics.drawString("中",10,drawY);
// 保存验证码的值,登录时校验
StringBuffer buffer = new StringBuffer();
int random = 0;
int len = chars.length;
for (int i = 0; i < charCount ; i++) {
random = new Random().nextInt(len);
buffer.append(chars[random]);
graphics.setColor(makeColor());
graphics.drawString(chars[random],(i+1)*space, drawY);
}
// 绘制干扰线
for (int i = 0; i < 4; i++) {
graphics.setColor(makeColor());
int[] lines = makeLine();
graphics.drawLine(lines[0],lines[1],lines[2],lines[3]);
}
// 设置取消缓存
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control","no-cache");
response.setDateHeader("Expires",0);
response.setContentType("image/png");
// 生成的验证码存在session中
HttpSession session = request.getSession();
session.setAttribute("captcha",buffer.toString());
ServletOutputStream outputStream = response.getOutputStream();
/**
*
*/
ImageIO.write(image, "png", outputStream);
outputStream.flush();
outputStream.close();
}
// 获取随机颜色
private Color makeColor(){
Random random = new Random();
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
return new Color(r,g,b);
}
// 获取干扰线
private int[] makeLine(){
Random random = new Random();
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
return new int[]{x1,x2,y1,y2};
}
}
复制代码
创建验证码图片的步骤
创建图像类
获取画笔,在 Image 上画内容
设置图片背景色
创建 Font,书写文字
文字内容保存在 Session 中
返回 Image 在自定义的安全配置中添加验证码访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 增加js静态资源的访问权限,验证码访问权限,登录首页访问权限
.antMatchers("/login.html","/index.html","/login","/js/**","/captcha/**").permitAll()
// 给url配置角色访问权限
.antMatchers("/access/user").hasRole("USER")
.antMatchers("/access/read").hasRole("READ")
.antMatchers("/access/admin").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(custSuccessHandler) //执行验证成功的handler
.failureHandler(custFailureHandler) // 执行验证失败后的handler
// 指定使用自定义的登录界面
.loginPage("/login.html")
.loginProcessingUrl("/login")
.and()
.csrf().disable();
}
复制代码
在登录页面中增加验证码图片和输入框
<div id="validatePanel" class="item" style="width: 137px;">
<input type="text" id="captcha" placeholder="请输入验证码" maxlength="4">
<a href="javascript:void(0)" onclick="refreshCaptcha()"><img id="refreshCaptcha" class="validateImg" src="/captcha/create" ></a>
</div>
复制代码
在 js 中增加刷新验证码的函数,点击验证码图片即可刷新验证码
<script>
function refreshCaptcha(){
// 刷新验证码
var url = "/captcha/create?t=" + new Date();
$("#refreshCaptcha").attr("src",url);
}
</script>
复制代码
修改 ajax 请求,增加发送验证码的内容
<script type="text/javascript">
$(function(){
//juqery的入口函数
$("#btnLogin").click(function(){
var uname = $("#username").val();
var pwd = $("#password").val();
// 获取输入的验证码
var captcha = $("#captcha").val();
$.ajax({
// 发送ajax请求显示状态已取消,通过添加一下代码可以成功获取响应
async: false,
url:"/login",
type:"POST",
data:{
"username":uname,
"password":pwd,
"captcha":captcha
},
dataType:"json",
success:function(resp){
alert(resp.code + " " + resp.msg)
}
})
})
});
</script>
复制代码
过滤器验证验证码内容,应该在验证用户名密码之前验证发送的验证码内容,在 Spring Security 的用户名密码过滤器 UsernamePasswordAuthenticationFilter 之前自定义一个过滤器
先在 common 包中定义一个异常类 VerificationException
public class VerificationException extends AuthenticationException {
public VerificationException(String detail, Throwable ex) {
super(detail, ex);
}
public VerificationException(String detail) {
super(detail);
}
public VerificationException(){
super("验证码错误,请重新输入");
}
}
复制代码
修改 CustFailureHandler,增加 Result 属性,在校验验证码失败时输出 json 格式的错误信息
@Component
public class CustFailureHandler implements AuthenticationFailureHandler {
private Result result;
public Result getResult() {
return result;
}
public void setResult(Result result) {
this.result = result;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException {
//当框架验证用户信息失败时执行的方法
response.setContentType("text/json;charset=utf-8");
if( result == null){
Result localResult = new Result();
localResult.setCode(1);
localResult.setError(1001);
localResult.setMsg("登录失败");
result = localResult;
}
OutputStream out = response.getOutputStream();
ObjectMapper om = new ObjectMapper();
om.writeValue(out,result );
out.flush();
out.close();
}
}
复制代码
接着在 common 包中定义一个 Filter
@Component
public class VerificationCaptchaFilter extends OncePerRequestFilter {
@Resource
private CustFailureHandler failureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 只有login()才需要这个过滤器
String requestURI = httpServletRequest.getRequestURI();
if (!"/login".equals(requestURI)){
// 过滤器正常执行,非登录操作不参与验证码操作
filterChain.doFilter(httpServletRequest,httpServletResponse);
} else {
try {
// 登录操作,需要验证码
verificationCode(httpServletRequest);
// 如果 验证通过
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (VerificationException e) {
Result result = new Result();
result.setCode(1);
result.setError(1002);
result.setMsg("验证码错误");
failureHandler.setResult(result);
failureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
}
private void verificationCode(HttpServletRequest httpServletRequest) throws VerificationException {
HttpSession session = httpServletRequest.getSession();
String requestCode = httpServletRequest.getParameter("captcha");
String sessionCode = "";
String captcha = (String) session.getAttribute("captcha");
if (captcha != null){
sessionCode = captcha;
}
System.out.println(requestCode);
if (!StringUtils.isEmpty(sessionCode)){
session.removeAttribute("captcha");
}
if (StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(sessionCode) || !requestCode.equalsIgnoreCase(sessionCode)){
throw new VerificationException();
}
}
}
复制代码
修改自定义的安全配置,将自定义的过滤器加入到过滤器链中,放在校验用户名密码之前
@Configuration
@EnableWebSecurity
public class CustSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private CustSuccessHandler custSuccessHandler;
@Resource
private CustFailureHandler custFailureHandler;
@Resource
private VerificationCaptchaFilter captchaFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 增加js静态资源的访问权限
.antMatchers("/login.html","/index.html","/login","/js/**","/captcha/**").permitAll()
// 给url配置角色访问权限
.antMatchers("/access/user").hasRole("USER")
.antMatchers("/access/read").hasRole("READ")
.antMatchers("/access/admin").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(custSuccessHandler) //执行验证成功的handler
.failureHandler(custFailureHandler) // 执行验证失败后的handler
// 指定使用自定义的登录界面
.loginPage("/login.html")
.loginProcessingUrl("/login")
.and()
.csrf().disable();
// 在框架的过滤器Chain中加入自定义的过滤器
http.addFilterBefore(captchaFilter,UsernamePasswordAuthenticationFilter.class);
}
}
复制代码
重新启动应用
验证码生效
系列完结,撒花🎉!!!
划线
评论
复制
发布于: 刚刚阅读数: 3
版权声明: 本文为 InfoQ 作者【小白】的原创文章。
原文链接:【http://xie.infoq.cn/article/3993cbb132958fede4e385737】。文章转载请联系作者。
小白
关注
QA 2019.08.05 加入
微信号JingnanSJ或者公众号RiemannHypo获取异步和图灵系列书籍
评论