SpringBoot系列(七)SpringBoot 集成 Security 实现登录时增加图片验证码校验

上一节中我们基于 security 中默认提供的认证流程实现了登录功能。但是这个默认的实现只提供了用户名和密码登录,很显然在日常的项目中无法满足我们的需求。因此在这一节中将实现一个图形验证码的登录功能。在开始之前我们需要了解一下 security 的工作机制。

当我们发生一次请求时,首先会经过一组过滤器,security 就是基于这条过滤器链完成的。这条过滤器链在 spring boot 启动的时候就为我们填充完成了,而且顺序也是固定的。因此,我们的验证码实现的思路就很简单了,只需要在校验用户名和密码之前插入一个验证码的过滤器就可以实现了。关于过滤器链,这里可以找到
UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 中的doFilter 方法,打断点,执行登录操作,可以看到,如下:
在这里插入图片描述
我们的目标就是在UsernamePasswordAuthenticationFilter 图中标红的位置插入验证码的过滤器,校验成功之后执行下一个过滤器,校验失败直接停止。这个就是我插入的一个验证码的过滤器。

ValidateCodeAuthenticationFilter 过滤器

public class ValidateCodeAuthenticationFilter extends OncePerRequestFilter {
    private static final Logger log = LoggerFactory.getLogger(ValidateCodeAuthenticationFilter.class);
    private static final String SPRING_SECURITY_FORM_VALIDATE_CODE_KEY = "validateCode";
    private ValidateCodeProcessor validateCodeProcessor;
    private AuthenticationFailureHandler authenticationFailureHandler;
    private List<String> authorizeRequests;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //验证该请求我们是否进行拦截,不拦截这里直接执行下一个 Filter
        if (checkMatch(request)) {
            //获取验证码
            String validateCode = obtainValidateCode(request);
            if (validateCode == null) {
                validateCode = "";
            }

            validateCode = validateCode.trim();

            try {
                //进行验证,验证失败抛出异常
                validateCodeProcessor.verification(request, validateCode);
            } catch (ValidateCodeException e) {
                // 这里捕获验证失败异常,停止执行下一个Filter
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        filterChain.doFilter(request, response);
    }

    private String obtainValidateCode(HttpServletRequest request) {
        return request.getParameter(SPRING_SECURITY_FORM_VALIDATE_CODE_KEY);
    }

    private boolean checkMatch(HttpServletRequest request) {
        String url = request.getRequestURI();
        log.debug("Request URL is :[{}]", url);
        return authorizeRequests.contains(url);
    }

    public void setValidateCodeProcessor(ValidateCodeProcessor validateCodeProcessor) {
        this.validateCodeProcessor = validateCodeProcessor;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public void setAuthorizeRequests(List<String> authorizeRequests) {
        this.authorizeRequests = authorizeRequests;
    }
}
  1. 继承了OncePerRequestFilter 保证只执行一次
  2. checkMatch(request) 方法检查是否符合拦截规则,这里我们只拦截登录请求的url
  3. validateCodeProcessor.verification(request, validateCode) 不抛出异常,则代表校验通过,直接执行下一个过滤器,失败的时候通过authenticationFailureHandler返回登录失败结果。

添加 ValidateCodeAuthenticationFilter 到UsernamePasswordAuthenticationFilter之前。
AbstractPreAuthenticatedProcessingFilterUsernamePasswordAuthenticationFilter 父类

public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private Filter validateCodeAuthenticationFilter;

    public ValidateCodeSecurityConfig(Filter validateCodeAuthenticationFilter) {
        this.validateCodeAuthenticationFilter = validateCodeAuthenticationFilter;
    }

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        builder.addFilterBefore(validateCodeAuthenticationFilter, AbstractPreAuthenticatedProcessingFilter.class);
    }
}

应用ValidateCodeSecurityConfig配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PropertySource("classpath:security-config.properties")
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> validateCodeConfigurerAdapter;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                //省略 loginform()
                .and().apply(validateCodeConfigurerAdapter)
                //省略其他配置项
    }

到这里我们的验证码过滤器已经完成了,下面我们去实现一个生成验证码的接口

@Slf4j
@RestController
@RequestMapping("${spring.security.code.servlet.path:/}")
public class ValidateCodeGeneratorController {
    private ValidateCodeProcessor validateCodeProcessor;

    public ValidateCodeGeneratorController(ValidateCodeProcessor validateCodeProcessor) {
        this.validateCodeProcessor = validateCodeProcessor;
    }

    @GetMapping(value = "/refresh", produces = MediaType.IMAGE_PNG_VALUE)
    public byte[] getValidateCode(HttpServletRequest request) {
        //生成验证码
        ImageCode code = (ImageCode) validateCodeProcessor.send();
        //将验证码存储到session中
        validateCodeProcessor.store(request, code);
        //返回验证码图片的字节数组给页面
        return code.getImage();
    }
}

验证码处理器

public abstract class AbstractValidateCodeProcessor implements ValidateCodeProcessor {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractValidateCodeProcessor.class);
    private ValidateCodeStorage validateCodeStorage;
    private ValidateCodeProperties validateCodeProperties;

    public AbstractValidateCodeProcessor(ValidateCodeStorage validateCodeStorage, ValidateCodeProperties validateCodeProperties) {
        this.validateCodeStorage = validateCodeStorage;
        this.validateCodeProperties = validateCodeProperties;
    }

    @Override
    public ValidateCode generator() {
        //生成随机字母和数字
        String code = RandomStringUtils.randomAlphanumeric(validateCodeProperties.getLength());
        long currentTime = System.currentTimeMillis();
        //设置验证码过期时间
        ValidateCode validateCode = new ValidateCode(code, new Date(currentTime + validateCodeProperties.getExpireInSecond() * 1000));
        LOG.info("The validate code is generated:[{}]", validateCode.getValidateCode());
        return validateCode;
    }

    @Override
    public void verification(HttpServletRequest request, String code) throws ValidateCodeException {
        //从session中取出验证码,这里的 validateCodeStorage为接口 之后可以扩展为 基于redis的
        ValidateCode validateCode = validateCodeStorage.get(request);
        if (validateCode == null) {
            throw new ValidateCodeNotFoundException("The captcha not found");
        }
        if (validateCode.getExpireTime().before(new Date())) {
            throw new ValidateCodeExpireException("The captcha has expired");
        }

        if (!validateCode.getValidateCode().equalsIgnoreCase(code)){
            throw new ValidateCodeNotMatchedException("The captcha not matched");
        }
    }

    @Override
    public boolean store(HttpServletRequest request, ValidateCode code) {
        //向session写入验证码
        return validateCodeStorage.set(request, code);
    }
}

图片验证码生成操作

public class ImageCodeGenerator extends AbstractValidateCodeProcessor {
    private ImageDrawer imageDrawer;

    public ImageCodeGenerator(ValidateCodeStorage validateCodeStorage, ValidateCodeProperties validateCodeProperties, ImageDrawer imageDrawer) {
        super(validateCodeStorage, validateCodeProperties);
        this.imageDrawer = imageDrawer;
    }

    @Override
    public Object send() {
        ValidateCode validateCode = this.generator();
        return imageDrawer.draw(validateCode);
    }
}
public class DefaultImageDrawer implements ImageDrawer {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultImageDrawer.class);
    private ImageCodeProperties imageCodeProperties;

    public DefaultImageDrawer(ImageCodeProperties imageCodeProperties) {
        this.imageCodeProperties = imageCodeProperties;
    }

    @Override
    public ImageCode draw(ValidateCode code) {
        return generateValidateCode(code);
    }

    private ImageCode generateValidateCode(ValidateCode validateCode) {
        int width = imageCodeProperties.getWidth();
        int height = imageCodeProperties.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
        String code = validateCode.getValidateCode();
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < code.length(); i++) {
            String rand = String.valueOf(code.charAt(i));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();
        byte[] imageBuffer = null;
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()){
            ImageIO.write(image, "PNG", out);
            imageBuffer = out.toByteArray();
        }catch (IOException e) {
            LOG.error("Draw image failed!");
        }
        return new ImageCode(sRand.toString(), validateCode.getExpireTime() , imageBuffer);
    }

    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

我们可以展示下效果
在这里插入图片描述
后端项目地址:https://github.com/QUPENGRANDOM/vd-mall

前端项目地址:https://github.com/QUPENGRANDOM/vd-mall-web

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,下面是 SpringBoot 集成 SpringSecurity 实现登录和权限管理的示例代码: 首先,我们需要在 pom.xml 中添加 SpringSecurity 的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 然后,我们需要创建一个继承了 WebSecurityConfigurerAdapter 的配置类,并且使用 @EnableWebSecurity 注解启用 SpringSecurity: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasRole("USER") .anyRequest().authenticated() .and() .formLogin().loginPage("/login").permitAll() .and() .logout().permitAll(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` 在上面的代码中,我们通过 configure(AuthenticationManagerBuilder auth) 方法指定了使用哪个 UserDetailsService 来获取用户信息,通过 configure(HttpSecurity http) 方法配置了哪些 URL 需要哪些角色才能访问,以及登录页面和退出登录的 URL。 接下来,我们需要实现 UserDetailsService 接口,用来获取用户信息: ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : user.getRoles()) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } } ``` 在上面的代码中,我们通过 UserRepository 来获取用户信息,并且将用户的角色转换成 GrantedAuthority 对象。 最后,我们需要创建一个控制器来处理登录和退出登录的请求: ```java @Controller public class LoginController { @GetMapping("/login") public String login() { return "login"; } @GetMapping("/logout") public String logout(HttpServletRequest request, HttpServletResponse response) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { new SecurityContextLogoutHandler().logout(request, response, authentication); } return "redirect:/login?logout"; } } ``` 在上面的代码中,我们通过 @GetMapping 注解来处理登录和退出登录的请求,并且在退出登录成功后重定向到登录页面。 以上就是 SpringBoot 集成 SpringSecurity 实现登录和权限管理的示例代码,希望对你有所帮助。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值