SpringBoot集成Spring Security(4)——自定义表单登录

SpringBoot集成Spring Security(4)——自定义表单登录

1.添加验证码功能

1.1 验证码程序
 从网上拉取一段建立验证码的程序:

package cn.it.test.Servlet;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;


public class VerifyServlet extends HttpServlet {

    private static final long serialVersionUID = -5051097528828603895L;

    /**
     * 验证码图片的宽度。
     */
    private int width = 100;

    /**
     *  验证码图片的高度。
     */
    private int height = 30;

    /**
     * 验证码字符个数
     */
    private int codeCount = 4;

    /**
     * 字体高度
     */
    private int fontHeight;

    /**
     * 干扰线数量
     */
    private int interLine = 16;

    /**
     * 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数
     */
    private int codeX;

    /**
     * codeY ,验证字符的y轴值,因为并行所以值一样
     */
    private int codeY;

    /**
     * codeSequence 表示字符允许出现的序列值
     */
    char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
            'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

    /**
     * 初始化验证图片属性
     */
    @Override
    public void init() throws ServletException {
        // 从web.xml中获取初始信息
        // 宽度
        String strWidth = this.getInitParameter("width");
        // 高度
        String strHeight = this.getInitParameter("height");
        // 字符个数
        String strCodeCount = this.getInitParameter("codeCount");
        // 将配置的信息转换成数值
        try {
            if (strWidth != null && strWidth.length() != 0) {
                width = Integer.parseInt(strWidth);
            }
            if (strHeight != null && strHeight.length() != 0) {
                height = Integer.parseInt(strHeight);
            }
            if (strCodeCount != null && strCodeCount.length() != 0) {
                codeCount = Integer.parseInt(strCodeCount);
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        //width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
        //codeCount+1     //等比分配显示的宽度,包括左右两边的空格
        codeX = (width-4) / (codeCount+1);
        //height - 10 集中显示验证码
        fontHeight = height - 10;
        codeY = height - 7;
    }

    /**
     * @param request
     * @param response
     * @throws ServletException
     * @throws java.io.IOException
     */
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
        // 定义图像buffer
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D gd = buffImg.createGraphics();
        // 创建一个随机数生成器类
        Random random = new Random();
        // 将图像填充为白色
        gd.setColor(Color.LIGHT_GRAY);
        gd.fillRect(0, 0, width, height);
        // 创建字体,字体的大小应该根据图片的高度来定。
        Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
        // 设置字体。
        gd.setFont(font);
        // 画边框。
        gd.setColor(Color.BLACK);
        gd.drawRect(0, 0, width - 1, height - 1);
        // 随机产生16条干扰线,使图象中的认证码不易被其它程序探测到。
        gd.setColor(Color.gray);
        for (int i = 0; i < interLine; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            gd.drawLine(x, y, x + xl, y + yl);
        }
        // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
        StringBuffer randomCode = new StringBuffer();
        int red = 0, green = 0, blue = 0;
        // 随机产生codeCount数字的验证码。
        for (int i = 0; i < codeCount; i++) {
            // 得到随机产生的验证码数字。
            String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
            // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
            red = random.nextInt(255);
            green = random.nextInt(255);
            blue = random.nextInt(255);
            // 用随机产生的颜色将验证码绘制到图像中。
            gd.setColor(new Color(red,green,blue));
            gd.drawString(strRand, (i + 1) * codeX, codeY);
            // 将产生的四个随机数组合在一起。
            randomCode.append(strRand);
        }
        // 将四位数字的验证码保存到Session中。
        HttpSession session = request.getSession();
        session.setAttribute("validateCode", randomCode.toString());
        // 禁止图像缓存。
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        response.setContentType("image/jpeg");
        // 将图像输出到Servlet输出流中。
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(buffImg, "jpeg", sos);
        sos.close();
    }
}


1.2 在springboo的启动类中注入该类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {

        SpringApplication.run(Application.class,args);
    }
    /*
        注入验证码Servlet
     */
    @Bean
    public ServletRegistrationBean servletRegistration(){
        ServletRegistrationBean registration=new ServletRegistrationBean(new VerifyServlet());
        registration.addUrlMappings("/getVerifyCode");
        return registration;
    }
}

1.3 修改login.html文件
  添加验证码分区:

<div>
        <input type="text" class="form-control" name="verifyCode" required="required" placeholder="验证码">
        <img src="getVerifyCode" title="看不清,换一张" onclick="refresh(this)" onmouseover="mouseover(this)">
</div>
<script>
    function refresh(obj) {
        obj.src="getVerifyCode?"+Math.random();
    }

    function mouseover(obj) {
        obj.style.cursor="pointer";
    }
</script>

  placeholder:输入框有提示的值作为说明
  事件onmouseover()代表的是当鼠标放在验证码的图片上触发的事件
  obj.style.cursor="pointer" 即将鼠标的样式改成手指的形式
1.4 添加匿名访问 Url
WebSecurityConfig中允许该 Url 的匿名访问,不然没有登录是没有办法访问的:
在这里插入图片描述
1.5 运行程序
在这里插入图片描述
验证的方式:

  • 登录表单提交前发送 AJAX 验证验证码
  • 使用自定义过滤器(Filter),在 Spring security 校验前验证验证码合法性,在进行用户名和密码的验证
  • 和用户名、密码一起发送到后台,在 Spring security 中进行验

2.AJAX验证

  实际就是前端表单利用Http的post方法发送到后端进行逻辑的校验

3. 设置过滤器验证

  使用过滤器的思路是:在 Spring Security 处理登录验证请求前,验证验证码,如果正确,放行;如果不正确,调到异常。

3.1 编写验证码过滤器
  自定义一个过滤器,实现 OncePerRequestFilter 类(该 Filter 保证每次请求一定会过滤),在 isProtectedUrl() 方法中拦截了 /login 请求的POST方式。从request中取出验证码进行验证。
  程序:

/**
 * OncePerRequestFilter:每次请求一定会产生过滤
 */
public class VerifyFilter extends OncePerRequestFilter {
    private static final PathMatcher pathMatcher = new AntPathMatcher();
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (isProtectedUrl(request)){
            String verifyCode=request.getParameter("verifyCode");
            if(!validateVerifyCode(verifyCode,request)){
                //手动设置异常
                request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new DisabledException("验证码输入有误"));
                //转发错误的信息
                request.getRequestDispatcher("/login/error").forward(request,response);
            }else{
                filterChain.doFilter(request,response);
            }
        }else{
            filterChain.doFilter(request,response);
        }
    }

    //验证码校验
    private boolean validateVerifyCode(String verifyCode,HttpServletRequest request){
        //获取当前线程绑定的request对象
        //HttpServletRequest request= ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        String validateCode= ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
        verifyCode=verifyCode.toLowerCase();
        return  verifyCode.equals(validateCode);
    }

    //拦截login的post请求,路径地址也需要进行比较验证
    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && pathMatcher.match("/login",request.getServletPath());
    }
}

3.2 注入过滤器
  修改 WebSecurityConfig 的 configure 方法,添加一个 addFilterBefore() ,具有两个参数,作用是在参数二之前执行参数一设置的过滤器。Spring Security 对于用户名/密码登录方式是通过 UsernamePasswordAuthenticationFilter 处理的,我们在它之前执行验证码过滤器即可。
在这里插入图片描述
3.3 运行程序
在这里插入图片描述

4. Spring Security 验证

  使用过滤器验证和 AJAX 验证差别不大:

  • AJAX 是在提交前发一个请求,请求返回成功就提交,否则不提交;
  • 过滤器是先验证验证码,验证成功就让 Spring Security 验证用户名和密码;验证失败,则产生异常。

  如果用户需要验证更多的字段,就会使过滤器的逻辑性复杂起来,这时候就可以考虑使用Spring Security
4.1 WebAuthenticationDetails
  从spring security的UserDetailsService中发现默认只处理用户名和密码信息。这时候就要使用WebAuthenticationDetails :该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息
在这里插入图片描述
  故我们需要自定义一个类来实现WebAuthenticationDetails ,并在其中加入验证码信息:

/**
 * 获取客户的额外信息
 * WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息
 */
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
    private static final long serialVersionUID = 6975601077710753878L;
    private final String verifyCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        //获取页面输入的验证码
        verifyCode=request.getParameter("verifyCode");
    }

    public String getVerifyCode(){
        return this.verifyCode;
    }

    @Override
    public String toString() {
        StringBuffer sb=new StringBuffer();
        sb.append(super.toString()).append(";verifyCode:").append(this.getVerifyCode());
        return sb.toString();
    }
}

可以从前台的表单中获得输入的验证码verifyCode,并通过get()方法方便调用
4.2 AuthenticationDetailsSource
  上一步自定义了WebAuthenticationDetails,还需要将其放入 AuthenticationDetailsSource 中来替换原本的 WebAuthenticationDetails ,因此还得实现自定义 CustomAuthenticationDetailSource

/**
 * 用于spring security在用户登录时对用户信息进行填充
 */
@Component("authenticationDetailSource")
public class CustomAuthenticationDetailSource implements AuthenticationDetailsSource <HttpServletRequest,WebAuthenticationDetails>{

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request){
        return new CustomWebAuthenticationDetails(request);
    }
}

  (1).该类内容将原本的 WebAuthenticationDetails 替换为了我们的CustomAuthenticationDetailSource
  (2).然后我们将 CustomAuthenticationDetailsSource 注入Spring Security中,替换掉默认的 AuthenticationDetailsSource
  (3),修改 WebSecurityConfig,将其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它。

    //额外的用户信息
    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailSource;
    

在这里插入图片描述
4.3 AuthenticationProvider
  通过自定义WebAuthenticationDetailsAuthenticationDetailsSource将验证码和用户名、密码一起带入了Spring Security中,下面将它取出来。
  这里需要自定义AuthenticationProvider,需要注意的是,如果是我们自己实现AuthenticationProvider,那么就需要自己做密码校验了。

/**
 * 自定义进行逻辑验证
 */
@Component
public class CustomAuthenticationProvide implements AuthenticationProvider {

    //用户基本信息:用户表,角色表,用户-角色表
    @Autowired
    private CustomerUserDetailsService userDetailsService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //获取用户的用户名和密码
        String username=authentication.getName();
        String password=authentication.getCredentials().toString();
        //获取用户的额外验证信息
        CustomWebAuthenticationDetails details= (CustomWebAuthenticationDetails) authentication.getDetails();

        //获取验证码
        String verifyCode=details.getVerifyCode();
        if(!validateVerifyCode(verifyCode)){
            throw new DisabledException("验证码输入有误");
        }
        //获取数据库中能查询到的用户信息
        UserDetails userDetails=userDetailsService.loadUserByUsername(username);

        // 如果是自定义AuthenticationProvider,需要手动密码校验
        if (!userDetails.getPassword().equals(password)){
            throw new BadCredentialsException("密码错误");
        }

        return new UsernamePasswordAuthenticationToken(userDetails,password,userDetails.getAuthorities());
    }

    //表单输入的verifyCode
    private boolean validateVerifyCode(String verifyCode) {
        //获取当前线程的request
        HttpServletRequest request= ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        //从session中获取动态验证码
        String validateCode=request.getSession().getAttribute("validateCode").toString().toLowerCase();
        verifyCode=verifyCode.toLowerCase();
        System.out.println("系统验证码:"+validateCode+"用户输入验证码:"+verifyCode);
        return validateCode.equals(verifyCode);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

  最后在 WebSecurityConfig 中将其注入,并在 config 方法中通过auth.authenticationProvider() 指定使用。
在这里插入图片描述
4.4 程序运行
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值