Springboot+SpringSecurity实现图片验证码功能

到目前,我学会了两种方法,在这里分享给大家。

第一种


在使用Spring Security框架过程中,经常会有这样的需求,即在登录验证时,附带增加额外的数据,如验证码、用户类型等。下面将介绍如何实现。

第一步:实现自定义的WebAuthenticationDetails

       该类提供了获取用户登录时携带的额外信息的功能,默认实现WebAuthenticationDetails提供了remoteAddress与sessionId信息。开发者可以通过Authentication的getDetails()获取WebAuthenticationDetails。我们编写自定义类CustomWebAuthenticationDetails继承自WebAuthenticationDetails,添加我们关心的数据,以字段“demo"来表示。

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
   
    private static final long serialVersionUID = 6975601077710753878L;
    private final String demo;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        demo = request.getParameter("demo");
    }

    public String getDemo() {
        return demo;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("; Demo: ").append(this.getDemo());
        return sb.toString();
    }
}

注:在登录页面,可将token字段放在form表单中,也可以直接加在url的参数中,进而把额外数据发送给后台。

第二步:实现自定义的AuthenticationDetailsSource

       该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充,默认实现是WebAuthenticationDetailsSource,生成上面的默认实现WebAuthenticationDetails。我们编写类实现AuthenticationDetailsSource,用于生成上面自定义的CustomWebAuthenticationDetails。
 

@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

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

第三步:配置使用自定义的AuthenticationDetailsSource

只要看这一句.formLogin().authenticationDetailsSource(authenticationDetailsSource)

别忘了注入:

@Autowired
    private CustomAuthenticationDetailsSource authenticationDetailsSource;

第四步:实现自定义的AuthenticationProvider

AuthenticationProvider提供登录验证处理逻辑,我们实现该接口编写自己的验证逻辑。

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private HttpServletRequest request;
    
    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();  // 如上面的介绍,这里通过authentication.getDetails()获取详细信息
        // System.out.println(details); details.getRemoteAddress(); details.getSessionId(); details.getDemo();
        // 下面是验证逻辑,验证通过则返回UsernamePasswordAuthenticationToken,
        // 否则,可直接抛出错误(AuthenticationException的子类,在登录验证不通过重定向至登录页时可通过session.SPRING_SECURITY_LAST_EXCEPTION.message获取具体错误提示信息)
        if(!request.getSession().getAttribute("text").toString().equalsIgnoreCase(details.getDemo()))//用户输入的和验证码不一致
        {
            System.out.println(request.getSession().getAttribute("text").toString()+"    "+details.getDemo());
            throw new BadCredentialsException("验证码不正确");
        }
    }

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

}

第五步:配置使用自定义的AuthenticationProvider

@Override
    public void configure(AuthenticationManagerBuilder auth)throws Exception{
        auth.authenticationProvider(myAuthenticationProvider);
    }

第二种


第一步:写个产生验证码的类:

public class VerifyCode {
    private int w = 70;
    private int h = 35;
    private Random r = new Random();
    // {"宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312"}
    private String[] fontNames  = {"宋体", "华文楷体", "黑体", "微软雅黑", "楷体_GB2312"};
    // 可选字符
    private String codes  = "0123456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";
    // 背景色
    private Color bgColor  = new Color(255, 255, 255);
    // 验证码上的文本
    private String text ;

    public VerifyCode() {
    }
    public VerifyCode(LocalDateTime localDateTime) {
        this.localDateTime = localDateTime;
    }
    public VerifyCode(int second) {
        // 多少秒后
        this.localDateTime = LocalDateTime.now().plusSeconds(second);
    }


    //过期时间
    private LocalDateTime localDateTime;
    // 生成随机的颜色
    private Color randomColor () {
        int red = r.nextInt(150);
        int green = r.nextInt(150);
        int blue = r.nextInt(150);
        return new Color(red, green, blue);
    }

    // 生成随机的字体
    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(5) + 24; //生成随机字号, 24 ~ 28
        return new Font(fontName, style, size);
    }

    // 画干扰线
    private void drawLine (BufferedImage image) {
        int num  = 3;//一共画3条
        Graphics2D g2 = (Graphics2D)image.getGraphics();
        for(int i = 0; i < num; i++) {//生成两个点的坐标,即4个值
            int x1 = r.nextInt(w);
            int y1 = r.nextInt(h);
            int x2 = r.nextInt(w);
            int y2 = r.nextInt(h);
            g2.setStroke(new BasicStroke(1.5F));
            g2.setColor(Color.BLUE); //干扰线是蓝色
            g2.drawLine(x1, y1, x2, y2);//画线
        }
    }

    // 随机生成一个字符
    private char randomChar () {
        int index = r.nextInt(codes.length());
        return codes.charAt(index);
    }

    // 创建BufferedImage
    private BufferedImage createImage () {
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = (Graphics2D)image.getGraphics();
        g2.setColor(this.bgColor);
        g2.fillRect(0, 0, w, h);
        return image;
    }

    // 调用这个方法得到验证码
    public BufferedImage getImage () {
        BufferedImage image = createImage();//创建图片缓冲区
        Graphics2D g2 = (Graphics2D)image.getGraphics();//得到绘制环境
        StringBuilder sb = new StringBuilder();//用来装载生成的验证码文本
        // 向图片中画4个字符
        for(int i = 0; i < 4; i++)  {//循环四次,每次生成一个字符
            String s = randomChar() + "";//随机生成一个字母
            sb.append(s); //把字母添加到sb中
            float x = i * 1.0F * w / 4; //设置当前字符的x轴坐标
            g2.setFont(randomFont()); //设置随机字体
            g2.setColor(randomColor()); //设置随机颜色
            g2.drawString(s, x, h-5); //画图
        }
        this.text = sb.toString(); //把生成的字符串赋给了this.text
        drawLine(image); //添加干扰线
        return image;
    }

    // 返回验证码图片上的文本
    public String getText () {
        return text;
    }

    // 保存图片到指定的输出流
    public static void output (BufferedImage image, OutputStream out)
            throws IOException {
        ImageIO.write(image, "JPEG", out);
    }
    public LocalDateTime getLocalDateTime() {
        return localDateTime;
    }

    public void setLocalDateTime(LocalDateTime localDateTime) {
        this.localDateTime = localDateTime;
    }
    public boolean isExpired(){
        return LocalDateTime.now().isAfter(localDateTime);
    }
}

第二步:写个代码抛出异常的封装类

public class ValidateCodeException extends AuthenticationException {

 
    private static final long serialVersionUID = 1L;

    public ValidateCodeException(String msg) {
        super(msg);
    }

}

第三步:配置验证码过滤器

在spring中,filter都默认继承OncePerRequestFilter,但为什么要这样呢?

OncePerRequestFilter顾名思义,他能够确保在一次请求只通过一次filter,而不需要重复执行

常识上都认为,一次请求本来就只过一次,为什么还要由此特别限定呢,实际上此方式是为了兼容不同的web container,特意而为之(jsr168),

也就是说并不是所有的container都像我们期望的只过滤一次,servlet版本不同,表现也不同

因此,为了兼容各种不同的运行环境和版本,默认filter继承OncePerRequestFilter是一个比较稳妥的选择

public class ValidateCodeFilter extends OncePerRequestFilter {
    private MyAuthenticationFailHander myAuthenticationFailHander;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (StringUtils.equals("/login/form", request.getRequestURI())
                &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
            try {
                System.out.println("验证码");
                validate(new ServletWebRequest(request));
            }
            catch (ValidateCodeException e) {
                System.out.println("验证码");
                myAuthenticationFailHander.onAuthenticationFailure(request, response, e);
                // 不继续执行
                return;
            }
        }
        // 继续执行下一步
        filterChain.doFilter(request, response);
    }
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        System.out.println("验证码");
        // 从Session中获取imageCode对象
        VerifyCode verifyCode = (VerifyCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        System.out.println("从session获取在imagecode对象"+verifyCode+"    "+codeInRequest);
        if (StringUtils.isBlank(codeInRequest)) {
            System.out.println("纳尼???为空");
            throw new ValidateCodeException("验证码为空或者不存在");
        }
        if(verifyCode == null){
            System.out.println("纳尼???不存在");
            throw new ValidateCodeException("验证码不存在,请刷新验证码");
        }
        if (verifyCode.isExpired()) {
            //从session移除过期的验证码
            System.out.println("纳尼???过期");
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码过期");
        }
        if (!StringUtils.equalsIgnoreCase(verifyCode.getText(), codeInRequest)) {
            System.out.println("纳尼???不匹配");
            throw new ValidateCodeException("验证码不匹配");
        }
        // session 中移除key
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }

    public MyAuthenticationFailHander getMyAuthenticationFailHander() {
        return myAuthenticationFailHander;
    }

    public void setMyAuthenticationFailHander(MyAuthenticationFailHander myAuthenticationFailHander) {
        this.myAuthenticationFailHander = myAuthenticationFailHander;
    }
}

上面的StringUtils.equals("/login/form", request.getRequestURI()中,引号里的是对应安全控制中心的loginProcessingUrl

第四步:生成验证码Control

@RestController
public class ValidateCodeController {
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    @GetMapping("/verifycode/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 1.根据随机数生成图片
        VerifyCode vc = new VerifyCode(60);//设置60秒过期
        /*ImageCode imageCode = createImageCode(request);*/
        // 2.将图片存入session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, vc);
        // 3.将生成的图片写入到接口响应中
        ImageIO.write(vc.getImage(), "JPEG", response.getOutputStream());
    }
}

第五步:SecurityConfig里进行过滤器配置

//验证码过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        //验证码过滤器中使用自己的错误处理
        validateCodeFilter.setMyAuthenticationFailHander(myAuthenticationFailHander);

第六步:前台 

<img src="/verifycode/image" style="margin-left: 30px;">
<input type="button" value="看不清? 换一张." style="margin-left: 30px;" id="btn">
				<script type="text/javascript">

                    document.getElementById("btn").onclick = function () {
                        // 获取img元素
                        // 为了让浏览器发送请求到servlet, 所以一定要改变src
                        document.getElementsByTagName("img")[0].src =
                            "/verifycode/image?time=" + new Date().getTime();
                    };

				</script>

效果展示:


好,到这里就完成了一个简陋的二维码的实现。

实现一个简单的登录功能,可以使用Spring Boot作为后端框架,Vue作为前端框架。以下是实现步骤: 1. 后端实现: - 使用Spring Boot创建一个新的项目。 - 添加Spring Security依赖,用于实现身份验证和授权。 - 创建一个用户实体类,包含用户名和密码等必要信息。 - 创建一个用户仓库(Repository),用于与数据库进行交互,保存和查询用户信息。 - 创建一个控制器(Controller),包含注册和登录的接口。 - 在登录的接口中,使用Spring Security进行身份验证,如果验证成功,则返回一个Token给前端。 - 在注册的接口中,保存用户信息到数据库。 2. 前端实现: - 使用Vue创建一个新的项目。 - 创建一个登录页面,包含用户名和密码的输入框,以及一个登录按钮。 - 当用户点击登录按钮时,发送一个登录请求给后端接口,并传递用户名和密码参数。 - 接收并解析后端返回的Token,保存到浏览器的本地存储(localStorage)中。 - 根据登录状态显示或隐藏相应的页面内容。 3. 集成前后端: - 在前端项目中,将后端接口的URL配置到前端的请求中。 - 在后端项目中,配置允许前端的跨域访问。 - 运行前后端项目,即可实现简单的登录功能。 需要注意的是,在实际项目中,登录功能还需要加上一些其他的安全措施,如密码加密、验证码验证、登录日志等。以上是一个简单的登录功能实现的基本步骤。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CtrlZ1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值