栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring全家桶-Spring Security之图片验证码

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力
Spring全家桶-Spring Security之图片验证码

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。


文章目录
  • Spring全家桶-Spring Security之图片验证码
  • 为什么需要验证码?
  • 一、验证码是什么?
  • 二、通过过滤器实现验证码
    • 使用步骤
      • 构建项目`spring-security-captcha-filter`
    • 1. 创建一个验证码的Filter
    • 2.创建一个验证码的Bean
    • 3.创建验证码请求
    • 4.登陆页
    • 运行验证
  • 三、自定义认证实现验证码
    • 使用步骤
      • 创建项目`spring-security-captcha-authentication`
      • 2.创建相关的bean
      • 3.创建`UserDetailsService`
      • 4.创建用户实体
      • 5.创建`CustomeAuthenticationProvider`认证
      • 创建`CustomWebAuthenticationDetails`
      • 创建`CustomWebAuthenticationSource`
      • 修改配置类`WebSecurityConfig`
    • 运行验证
  • 总结
    • 问题


为什么需要验证码?

在验证用户名和密码之前,引入辅助验证可有效防范暴力试错,图形验证码就是简单且行之有效的一种辅助验证方式,还有滑块验证码,机器人验证等方式。


一、验证码是什么?

全自动区分计算机和人类的公开图灵测试(英语:Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA),又称验证码,是一种区分用户是机器或人类的公共全自动程序。在CAPTCHA测试中,作为服务器的计算机会自动生成一个问题由用户来解答。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于机器无法解答CAPTCHA的问题,回答出问题的用户即可视为人类。---《维基百科》

二、通过过滤器实现验证码

自定义一个专门处理验证码逻辑的过滤器,将其添加到Spring Security过滤器链的合适位置。当匹配到登录请求时,立刻对验证码进行校验,成功则放行,失败则提前结束整个验证请求。

使用步骤 构建项目spring-security-captcha-filter

我们使用com.google.code.kaptcha进行验证码的生成。
官网地址:https://code.google.com/archive/p/kaptcha/wikis
项目的pom文件如下:


    org.springframework.boot
    spring-boot-starter-security



    org.springframework.boot
    spring-boot-starter-web



    com.google.code
    kaptcha

1. 创建一个验证码的Filter
public class CaptchaFilter extends OncePerRequestFilter {
    //spring security登陆接口地址
    public static final String LOGIN_URL = "/login";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //只有登陆的情况下需要校验验证码
       if(!Objects.equals(LOGIN_URL,request.getRequestURI())){
            filterChain.doFilter(request,response);
        }else{
            //校验验证码
            checkCaptcha(request);
            filterChain.doFilter(request,response);
        }
    }
    //校验验证码
    private void checkCaptcha(HttpServletRequest request) {
        String captcha = request.getParameter("captcha");
        HttpSession session = request.getSession();
        String catpchaSave = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        if(StringUtils.hasLength(catpchaSave)){
            session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
        }
        if(!StringUtils.hasLength(captcha) || !StringUtils.hasLength(catpchaSave) || !Objects.equals(captcha,catpchaSave)){
            throw  new RuntimeException("验证码输入错误");
        }
    }
}
2.创建一个验证码的Bean
@Bean
    public Producer captchaProducer(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width","150");
        properties.setProperty("kaptcha.image.height","15");
        properties.setProperty("kaptcha.textproducer.char.string","0123456789");
        properties.setProperty("kaptcha.textproducer.char.length","4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
3.创建验证码请求
@Controller
public class CaptchaController {
    @Autowired
    private Producer captchaProducer;
    @GetMapping("https://www.mshxw.com/file/tupian/20220524/captcha")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");
        String capText = captchaProducer.createText();
        request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();
        // write the data out
        ImageIO.write(bi, "jpg", out);
        try (out) {
            ImageIO.write(bi, "jpg", out);
            out.flush();
        }
    }
}
4.登陆页



    
    验证码登陆


运行验证

运行项目,访问http://localhost:8080/login.html,将出现如下页面:

输入默认的用户名和密码/验证码即可登陆,如果验证码不正确,将会出现相关的报错:

java.lang.RuntimeException: 验证码输入错误
	at org.tony.spring.security.config.CaptchaFilter.checkCaptcha(CaptchaFilter.java:47) ~[classes/:na]
	at org.tony.spring.security.config.CaptchaFilter.doFilterInternal(CaptchaFilter.java:34) ~[classes/:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar:5.3.18]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.6.2.jar:5.6.2]
三、自定义认证实现验证码 使用步骤 创建项目spring-security-captcha-authentication
  1. 创建项目的pom
    项目的pom和上面项目的pom文件一样
2.创建相关的bean
//配置验证码bean
 @Bean
    public Producer captchaProducer(){
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width","150");
        properties.setProperty("kaptcha.image.height","50");
        properties.setProperty("kaptcha.textproducer.char.string","0123456789");
        properties.setProperty("kaptcha.textproducer.char.length","4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

//创建userDetailsService
    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailsServiceImpl();
    }

//设置密码加密策略
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
3.创建UserDetailsService
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("admin");
        //设置相关密码,这里的密码一定要和加密策略中设置的密码一致
        userInfo.setPassword(new BCryptPasswordEncoder().encode("123456"));
        return userInfo;
    }
}
4.创建用户实体
public class UserInfo implements UserDetails {
	//密码
    private String password;
	//用户名
    private String username;

    public void setPassword(String password) {
        this.password = password;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    @Override
    public Collection getAuthorities() {
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
        List authorities =new ArrayList<>();
        authorities.add(authority);
        return authorities;
    }
    @Override
    public String getPassword() {
        return this.password;
    }
    @Override
    public String getUsername() {
        return this.username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

以上是创建相关的实体和UserDetailService,和相关的密码策略。

5.创建CustomeAuthenticationProvider认证

代码如下:

@Component
public class CustomeAuthenticationProvider extends DaoAuthenticationProvider {
	//这个地方将UserDetailsService和PasswordEncoder通过构造进行添加,这边要进行相关的定义和创建相应的Bean
    public CustomeAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder){
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //获取详细信息
        CustomWebAuthenticationDetails customWebAuthenticationDetails = (CustomWebAuthenticationDetails) authentication.getDetails();
        //校验相应的验证码正确性
        if(!Boolean.TRUE.equals(customWebAuthenticationDetails.getCaptchaCheck())){
            throw new RuntimeException("验证码不正确");
        }
        super.additionalAuthenticationChecks(userDetails,authentication);
    }
}
创建CustomWebAuthenticationDetails
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
	//验证码是否校验成功标记
    private Boolean captchaCheck;

    public Boolean getCaptchaCheck() {
        return this.captchaCheck;
    }

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        String captcha = request.getParameter("captcha");
        HttpSession session = request.getSession();
        String captchaFromSession = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        if(StringUtils.hasLength(captchaFromSession)){
            session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
            if(StringUtils.hasLength(captcha) && Objects.equals(captcha,captchaFromSession)){
                this.captchaCheck = true;
            }else{
                this.captchaCheck = false;
            }
        }
    }
}
创建CustomWebAuthenticationSource
@Component
public class CustomWebAuthenticationSource implements
        AuthenticationDetailsSource {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
}
修改配置类WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationDetailsSource customWebAuthenticationSource;

	//应用AuthenticationProvider,通过自定义AuthenticationProvider进行验证码验证
    @Autowired
    private AuthenticationProvider authenticationProvider;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/books/**").hasAnyRole("ADMIN")
                .antMatchers("/", "https://www.mshxw.com/file/tupian/20220524/captcha").permitAll()
                .anyRequest().authenticated()
                .and()
                //使用customWebAuthenticationSource
                .formLogin().authenticationDetailsSource(customWebAuthenticationSource).loginPage("/login.html").loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }
}
运行验证

运行项目,访问http://localhost:8080/login.html,将出现如下页面:

输入默认的用户名和密码/验证码即可登陆,如果验证码不正确,将会出现相关的报错:

java.lang.RuntimeException: 验证码输入错误
	at org.tony.spring.security.config.CaptchaFilter.checkCaptcha(CaptchaFilter.java:47) ~[classes/:na]
	at org.tony.spring.security.config.CaptchaFilter.doFilterInternal(CaptchaFilter.java:34) ~[classes/:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar:5.3.18]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.6.2.jar:5.6.2]

以上是实现验证码的两种方式,使用过滤器是比较简单的实现,属于Servlet层面, 简单、 易理解。 其实, Spring Security还提供了一种更优雅的实现图形验证码的方式, 即自定义认证。

总结

我们都知道,Spring Security是通过过滤器链进行不同的过滤器拦截。我们在HttpSecurity中可以配置过滤器,如CSRF、 CORS、 表单登录等。每一个配置对应一个过滤器链。所有我们通过过滤器可以做到验证码的校验拦截。我们创建拦截器继承OncePerRequestFilter进行清关的处理。
OncePerRequestFilter旨在保证在任何servlet容器上每请求分派一次执行的过滤器基类。它提供了一个doFilterInternal(javax.servlet.http。HttpServletRequest javax.servlet.http。HttpServletResponse, javax.servlet.FilterChain)方法,带有HttpServletRequest和HttpServletResponse参数.
子类可以使用isAsyncDispatch(httpservletrequest)来确定何时调用过滤器作为异步调度的一部分,并使用isAsyncStarted(HttpServletRequest request)来确定何时将请求放置在异步模式下,因此当前派遣派遣何时是异常派遣模式对于给定的请求。在其自身线程中也出现的另一种调度类型是ERROR。如果子类希望在错误派遣期间应调用一次,则可以替代他们.

我们所面对的系统中的用户, 在Spring Security中被称为主体(principal) 。主体包含了所有能够经过验证而获得系统访问权限的用户、 设备或其他系统。 主体的概念实际上来自 Java Security,SpringSecurity通过一层包装将其定义为一个Authentication。

public interface Authentication extends Principal, Serializable {
	//获取权限列表
    Collection getAuthorities();
	//获取凭据
    Object getCredentials();
	//获取详细信息
    Object getDetails();
	//获取主体
    Object getPrincipal();
	//是否认证成功
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

由于大部分场景下身份验证都是基于用户名和密码进行的,因此Spring Security为我们提供了UsernamePasswordAuthenticationToken进行用户和密码的认证。在使用的表单登录中, 每一个登录用户都被包装为一个 ·UsernamePasswordAuthenticationToken·, 从而在Spring Security的各个AuthenticationProvider中进行使用。

public interface AuthenticationProvider {
	//认证成功,返回认证信息
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
	//是否支持验证当前的Authentication
    boolean supports(Class authentication);
}

一次完整的认证可以包含多个AuthenticationProvider。通过ProviderManager进行管理。

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        Iterator var9 = this.getProviders().iterator();
		//迭代验证每个AuthenticationProvider,直到有一个验证通过
        while(var9.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            if (provider.supports(toTest)) {
                if (logger.isTraceEnabled()) {
                    Log var10000 = logger;
                    String var10002 = provider.getClass().getSimpleName();
                    ++currentPosition;
                    var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
                }

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            } catch (ProviderNotFoundException var12) {
            } catch (AuthenticationException var13) {
                parentException = var13;
                lastException = var13;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

Spring Security提供了多种常见的认证技术, 包括但不限于以下几种:

  1. HTTP层面的认证, 包括HTTP基本认证和HTTP摘要认证
  2. 基于LDAP认证
  3. 证明用户身份的OpenID认证
  4. 授权的OAuth认证
  5. 基于数据库用户名和密码认证
    Spring Security为我们提供和一个抽象的认证AbstractUserDetailsAuthenticationProvider.
    在 AbstractUserDetailsAuthenticationProvider 中实现了基本的认证流程, 通过继承AbstractUserDetailsAuthenticationProvider, 并实现retrieveUser和additionalAuthenticationChecks两个抽象方法即可自定义核心认证过程.
问题

通过maven下载不了验证码的jar包?
解决:通过取私服上下载下来,之后解压到指定文件夹中,之后执行maven命令就可以将相关的jar安装到本地仓库中了。命令如下:

mvn install:install-file -DgroupId=com.google.code -DartifactId=kaptcha -Dversion=2.3.2 -Dfile=/Users/xiell/Downloads/kaptcha-2.3.2.jar -Dpackaging=jar -DgeneratePom=true
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/926716.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号