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

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

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



为什么需要验证码?

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


一、验证码是什么?

全自动区分计算机和人类的公开图灵测试(英语: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文件如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>com.google.code</groupId>
    <artifactId>kaptcha</artifactId>
</dependency>

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("/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.登陆页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>验证码登陆</title>
</head>
<body>
<form action="/login" method="post">
    <label>用户名:</label>
    <label>
        <input type="text" name="username" />
    </label>
    <label>密码:</label>
    <label>
        <input type="password" name="password" />
    </label>
    <label>验证码:</label>
    <label>
        <input type="text" name="captcha" />
        <img id="captcha" src="/captcha" alt="captcha" height="50px" width="150px" style="margin-left: 15px;" onclick="refreshCaptcha()">
    </label>
    <button type="submit" >登陆</button>
</form>
</body>
<script>
	//刷新验证码
    function refreshCaptcha(){
        var img = document.getElementById("captcha");
        img.src = "/captcha";
    }
</script>
</html>

运行验证

运行项目,访问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<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
        List<GrantedAuthority> 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<HttpServletRequest,WebAuthenticationDetails> {

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

修改配置类WebSecurityConfig

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> 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("/", "/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<? extends GrantedAuthority> 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<? extends Authentication> 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, 并实现retrieveUseradditionalAuthenticationChecks两个抽象方法即可自定义核心认证过程.

问题

通过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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值