Spring Security实现添加图片验证功能

目录
  • 本章内容
  • 思路
  • 方案
    • 怎么将字符串变成图片验证码?
      • kaptcha这么玩
      • hutool这么玩
    • 传统web项目
      • 过滤器方式
      • 认证器方式
      • 总结下
    • 前后端分离项目
      • 基于过滤器方式
      • 基于认证器方式

本章内容

Spring security添加图片验证方式,在互联网上面有很多这种博客,都写的非常的详细了。本篇主要讲一些添加图片验证的思路。还有前后端分离方式,图片验证要怎么去处理?

  • 图片验证的思路
  • 简单的demo

思路

小白: "我们从总体流程上看图片验证在认证的哪一个阶段?"

小黑: "在获取客户输入的用户名密码那一阶段,而且要在服务器获取数据库中用户名密码之前。这是一个区间[获取请求用户名密码, 获取数据库用户名密码)

而在 Spring security中, 可以很明显的发现有两种思路。

  • 第1种思路是在拦截登录请求准备认证的那个过滤器。
  • 第2种思路是在那个过滤器背后的认证器。"

小白: "为什么是这个阶段呢? 不能是在判断密码验证之前呢?"

小黑: "你傻啊, 如果在你说的阶段, 服务器需要去数据库中获取用户信息, 这相当的浪费系统资源"

小白: "哦哦, 我错了, 让我屡屡整个流程应该是啥样"

小白: "我需要事先在后端生成一个验证码,然后通过验证码返回一张图片给前端。前端登录表单添加图片验证。用户输入图片验证后点击登录,会存放在request请求中, 后端需要从request请求中读取到图片验证,判断前后端验证码是否相同, 如果图片验证码相同之后才开始从数据库拿用户信息。否则直接抛出认证异常"

简单点: 数据库获取用户账户之前, 先进行图片验证码验证

方案

怎么将字符串变成图片验证码?

这轮子肯定不能自己造, 有就拿来吧你

  • kaptcha
  • hutool

kaptcha这么玩

<!--验证码生成器-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
    <exclusions>
        <exclusion>
            <artifactId>javax.servlet-api</artifactId>
            <groupId>javax.servlet</groupId>
        </exclusion>
    </exclusions>
</dependency>
@Bean
public DefaultKaptcha captchaProducer() {
    Properties properties = new Properties();
    properties.put("kaptcha.border", "no");
    properties.put("kaptcha.textproducer.char.length","4");
    properties.put("kaptcha.image.height","50");
    properties.put("kaptcha.image.width","150");
    properties.put("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.ShadowGimpy");
    properties.put("kaptcha.textproducer.font.color","black");
    properties.put("kaptcha.textproducer.font.size","40");
    properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
    //properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.DefaultNoise");
    properties.put("kaptcha.textproducer.char.string","acdefhkmnprtwxy2345678");
    DefaultKaptcha kaptcha = new DefaultKaptcha();
    kaptcha.setConfig(new Config(properties));
    return kaptcha;
}
@Resource
private DefaultKaptcha producer;
@GetMapping("/verify-code")
public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception {
    response.setContentType("image/jpeg");
    String text = producer.createText();
    session.setAttribute("verify_code", text);
    BufferedImage image = producer.createImage(text);
    try (ServletOutputStream outputStream = response.getOutputStream()) {
        ImageIO.write(image, "jpeg", outputStream);
    }
}

hutool这么玩

@GetMapping("hutool-verify-code")
public void getHtoolVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {
    CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 80);
    session.setAttribute("hutool_verify_code", circleCaptcha.getCode());
    response.setContentType(MediaType.IMAGE_PNG_VALUE);
    circleCaptcha.write(response.getOutputStream());
}

这俩随便挑选一个完事

前端就非常简单了

<form th:action="@{/login}" method="post">
    <div class="input">
        <label for="name">用户名</label>
        <input type="text" name="username" id="name">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密码</label>
        <input type="password" name="password" id="pass">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="code">验证码</label>
        <input type="text" name="code" id="code"><img src="/verify-code" alt="验证码">
        <!--<input type="text" name="code" id="code"><img src="/hutool-verify-code" alt="验证码">-->
        <span class="spin"></span>
    </div>
    <div class="button login">
        <button type="submit">
            <span>登录</span>
            <i class="fa fa-check"></i>
        </button>
    </div>
    <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</form>

传统web项目

我们现在根据上面的思路来设计设计该怎么实现这项功能

过滤器方式

/**
 * 使用 OncePerRequestFilter 的方式需要配置匹配器
 */
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
    private final String login;
    private static final AntPathRequestMatcher requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(this.login,
            "POST");
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (requiresAuthenticationRequestMatcher.matches(request)) {
            validateCode(request);
        }
        filterChain.doFilter(request, response);
    }
    private void validateCode(HttpServletRequest request) {
        HttpSession session = request.getSession();
        // 获取保存在session中的code
        String verifyCode = (String) session.getAttribute("verify_code");
        if (StringUtils.isBlank(verifyCode)) {
            throw new ValidateCodeException("请重新申请验证码!");
        }
        // 拿到前端的 code
        String code = request.getParameter("code");
        if (StringUtils.isBlank(code)) {
            throw new ValidateCodeException("验证码不能为空!");
        }
        // 对比
        if (!StringUtils.equalsIgnoreCase(code, verifyCode)) {
            throw new AuthenticationServiceException("验证码错误!");
        }
        // 删除掉 session 中的 verify_code
        session.removeAttribute("verify_code");
    }
}

虽然OncePerRequestFilter每次浏览器请求过来, 都会调用过滤器. 但是过滤器顺序是非常重要的

@Controller
@Slf4j
public class IndexController {
   @GetMapping("login")
   public String login() {
      return "login";
   }
   @GetMapping("")
   @ResponseBody
   public Principal index(Principal principal) {
      return principal;
   }
}
@Configuration
public class SecurityConfig {
	public static final String[] MATCHERS_URLS = {"/verify-code",
			"/css/**",
			"/images/**",
			"/js/**",
			"/hutool-verify-code"};
	public static final String LOGIN_PROCESSING_URL = "/login";
	public static final String LOGIN_PAGE = "/login";
	public static final String SUCCESS_URL = "/index";
	@Bean
	public ValidateCodeFilter validateCodeFilter() {
		return new ValidateCodeFilter(LOGIN_PROCESSING_URL);
	}
//    @Bean
//    public WebSecurityCustomizer webSecurityCustomizer() {
//        return web -> web.ignoring()
//                .antMatchers("/js/**", "/css/**", "/images/**");
//    }
	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
		httpSecurity
				.authorizeHttpRequests()
				.antMatchers(MATCHERS_URLS).permitAll()
				.anyRequest()
				.authenticated()
				.and()
				.formLogin()
				.loginPage(LOGIN_PAGE)
				.loginProcessingUrl(LOGIN_PROCESSING_URL)
				.defaultSuccessUrl(SUCCESS_URL, true)
				.permitAll()
				.and()
				.csrf()
				.disable();
		httpSecurity.addFilterBefore(validateCodeFilter(), UsernamePasswordAuthenticationFilter.class);
		return httpSecurity.build();
	}
}

小白: "我在网上看到有些网友并不是继承的OncePerRequestFilter接口啊?"

小黑: "是的, 有一部分朋友选择继承UsernamePasswordAuthenticationFilter"

小黑: "继承这个过滤器的话, 我们需要配置很多东西, 比较麻烦"

小白: "为什么要有多余的配置?"

小黑: "你想想, 你自定义的过滤器继承至UsernamePasswordAuthenticationFilter, 自定义的过滤器和原先的过滤器是同时存在的"

小黑: "没有为你自定义的过滤器配置对应的Configurer, 那么它里面啥也没有全部属性都是默认值, 不说别的, 下面AuthenticationManager至少要配置吧?"

小黑: "他可是没有任何默认值, 这样会导致下面这行代码报错"

小黑: "当然如果你有自定义属于自己的Configurer那没话说, 比如FormLoginConfigurer"

p>小黑: "默认这个函数需要HttpSecurity调用的, 我们自定义的Filter并没有重写Configurer这个环节"

小白: "哦, 我知道了, 那我就是要继承至UsernamePasswordAuthenticationFilter呢? 我要怎么做?"

小黑: "也行, 这样就可以不用配置AntPathRequestMatcher了"

public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
      HttpSession session = request.getSession();
      String sessionVerifyCode = (String) session.getAttribute(Constants.VERIFY_CODE);
      String verifyCode = request.getParameter(Constants.VERIFY_CODE);
      if (StrUtil.isBlank(sessionVerifyCode) || StrUtil.isBlank(verifyCode)
            || !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) {
         throw new ValidateCodeException("图片验证码错误, 请重新获取");
      }
      return super.attemptAuthentication(request, response);
   }
}
@Bean
public VerifyCodeFilter verifyCodeFilter() throws Exception {
   VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
   verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
   return verifyCodeFilter;
}

小黑: "这样就可以了"

小白: "也不麻烦啊"

小黑: "好吧, 好像是"

小白: "等等, 那SecurityFilterChain呢? 特别是formLogin()函数要怎么配置?"

httpSecurity.formLogin()
      .loginPage(loginPage)
      .loginProcessingUrl(loginUrl)
      .defaultSuccessUrl("/", true)
      .permitAll();
httpSecurity.addFilterBefore(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);

小白: "那我前端表单用户名和密码的input标签的name属性变成userpwd了呢? 也在上面formLogin上配置?"

小黑: "这里就有区别了, 明显只能在VerifyCodeFilter Bean上配置"

@Bean
public VerifyCodeFilter verifyCodeFilter() throws Exception {
   VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
   verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
   verifyCodeFilter.setUsernameParameter("user");
   verifyCodeFilter.setPasswordParameter("pwd");
   return verifyCodeFilter;
}

小白: "我还以为有多麻烦呢, 就这..."

小黑: "额, 主要是spring security的过滤器不能代替, 只能插入某个过滤器前后位置, 所以如果自定义过滤器就需要我们配置一些属性"

认证器方式

小白: "认证器要怎么实现图片验证呢?"

小黑: "说到认证的认证器, 一定要想到DaoAuthenticationProvider"

小黑: "很多人在基于认证器实现图片验证时, 都重写additionalAuthenticationChecks, 这是不对的"

小白: "那应该重写哪个方法 ?"

小黑: "应该重写下面那个函数"

小白: "等一下, 你注意到这个方法的参数了么? 你这要怎么从request中拿验证码?"

小黑: "有别的方法, 看源码"

public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      assert requestAttributes != null;
      HttpServletRequest request = requestAttributes.getRequest();
      String verifyCode = request.getParameter(Constants.VERIFY_CODE);
      String sessionVerifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE);
      if (StrUtil.isBlank(sessionVerifyCode) && StrUtil.isBlank(verifyCode)
            && !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) {
         throw new ValidateCodeException("图片验证码错误, 请重新获取");
      }
      return super.authenticate(authentication);
   }
}

小白: "哦, 我看到了, 没想到还能这样"

小白: "那你现在要怎么加入到Spring Security, 让它代替掉原本的DaoAuthenticationProvider呢?"

小黑: "这里有一个思路, 还记得AuthenticationManager的父子关系吧, 你看到父亲只有一个, 你看到儿子可以有几个?"

小白: "好像是无数个, 那我是不是可以这么写?"

/**
 * 往父类的 AuthenticationManager 里添加 authenticationProvider
 * 在源码里面是这样的AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class);
 *
 * @return
 * @throws Exception
 */
@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
    MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
    return authenticationProvider;
}
// 往子类AuthenticationManager里面添加的 authenticationProvider
httpSecurity.authenticationProvider(authenticationProvider());

小黑: "这上面的代码有问题, AuthenticationManger有父类和子类, 上面这段代码同时往父类和子类都添加MyDaoAuthenticationProvider, 这样MyDaoAuthenticationProvider会被执行两次, 但request的流只能执行一次, 会报错"

小黑: "我们可以这么玩"

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // 代码省略
    // 代码省略
    // 代码省略
    // 代码省略
    // 往子类AuthenticationManager里面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父类加载 DaoAuthenticationProvider
    AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
    // 但是这种方式可以将 parent Manager 设置为 null, 所以是可以的
    authenticationManagerBuilder.parentAuthenticationManager(null);
    MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
    authenticationManagerBuilder.authenticationProvider(authenticationProvider);
    http.authenticationManager(authenticationManagerBuilder.build());
    return http.build();
}

小黑: "SecurityFilterChain表示一个Filter集合, 更直接点就是子类的AuthenticationManager"

小黑: "所以这种玩法是给子类AuthenticationManager添加Provider, 但是它需要手动将parent置为 null, 否则父类的DaoAuthenticationProvider还是会执行, 最后报错信息就不对了, 本来应该是验证码错误, 将会变成用户名和密码错误"

小黑: "还有就是, 很多人很喜欢在旧版本像下面这么玩"

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
   MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
   authenticationProvider.setPasswordEncoder(passwordEncoder());
   authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
   return new ProviderManager(authenticationProvider);
}

小黑: "在新版本也类似的这么搞, 但这样是有区别的, 下面这种方式只会加入到spring Bean上下文, 但是不会加入到Spring Security中执行, 他是无效的"

@Bean
public ProviderManager providerManager() throws Exception {
   MyDaoAuthenticationProvider authenticationProvider = authenticationProvider();
   return new ProviderManager(authenticationProvider);
}

小黑: "在新版本中, 使用上面那段代码是一点用都没有"

public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
   MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
   authenticationProvider.setPasswordEncoder(passwordEncoder());
   authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
   return authenticationProvider;
}
// 往子类AuthenticationManager里面添加的 authenticationProvider
httpSecurity.authenticationProvider(authenticationProvider());

小黑: "上面这样做也是不行, 他还是会存在两个, 一个是MyDaoAuthenticationProvider(子类), 另一个是DaoAuthenticationProvider(父类)"

小白: "那最好的办法是什么?"

小黑: "直接将MyDaoAuthenticationProvider添加到Spring Bean上下文"

@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
    MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
    return authenticationProvider;
}

小白: "那还有别的思路么?"

小黑: "还有么? 不清楚了, 万能网友应该知道"

小白: "就这样设置就行了? 其他还需不需要配置?"

小黑: "其他和过滤器方式一致"

总结下

@Bean
public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
// 最好的办法就是直接MyDaoAuthenticationProvider加入到Spring Bean里面就行了, 其他都不要
MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
return authenticationProvider;
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 // 代码省略
 // 代码省略
 // 代码省略
 // 代码省略
 // 往子类AuthenticationManager里面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父类加载 DaoAuthenticationProvider
 AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
 // 但是这种方式可以将 parent Manager 设置为 null, 所以是可以的
 authenticationManagerBuilder.parentAuthenticationManager(null);
 MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
 authenticationProvider.setPasswordEncoder(passwordEncoder());
 authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
 authenticationManagerBuilder.authenticationProvider(authenticationProvider);
 http.authenticationManager(authenticationManagerBuilder.build());
 return http.build();
}

都是可以的, 一个往父类的AuthenticationManager添加MyDaoAuthenticationProvider, 另一个往子类添加, 设置父类为null

前后端分离项目

小白: "前后端分离和传统web项目的区别是什么?"

小黑: "请求request和响应response都使用JSON传递数据"

小白: "那我们分析源码时只要关注 requestresponse 咯, 只要发现存在request的读, 和 response的写通通都要重写一边"

小黑: "是的, 其实很简单, 无非是图片验证码改用json读, 认证时的读取usernamepassword也使用json读, 其次是出现异常需要响应response, 也改成json写, 认证成功和失败需要响应到前端也改成json写"

小白: "哦, 那只要分析过源码, 就能够完成前后端分离功能了"

小黑: "所以还讲源码么? "

小白: "不用, 非常简单"

基于过滤器方式

public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
   @Resource
   private ObjectMapper objectMapper;
   /**
    * 很多人这里同时支持前后端分离, 其实不对, 既然是前后端分离就彻底点
    * 但为了跟上潮流, 我这里也搞前后端分离
    *
    * @param request
    * @param response
    * @return
    * @throws AuthenticationException
    */
   @SneakyThrows
   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
      if (!"POST".equals(request.getMethod())) {
         throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
      }
      String contentType = request.getContentType();
      HttpSession session = request.getSession();
      if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
         Map map = objectMapper.readValue(request.getInputStream(), Map.class);
         imageJSONVerifyCode(session, map);
         String username = (String) map.get(this.getUsernameParameter());
         username = (username != null) ? username.trim() : "";
         String password = (String) map.get(this.getPasswordParameter());
         password = (password != null) ? password : "";
         UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
               password);
         // Allow subclasses to set the "details" property
         setDetails(request, authRequest);
         return this.getAuthenticationManager().authenticate(authRequest);
      }
      imageVerifyCode(request, session);
      return super.attemptAuthentication(request, response);
   }
   private void imageJSONVerifyCode(HttpSession session, Map map) throws ValidateCodeException {
      String verifyCode = (String) map.get(Constants.VERIFY_CODE);
      String code = (String) session.getAttribute(Constants.VERIFY_CODE);
      if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
         throw new ValidateCodeException("验证码错误, 请重新获取验证码");
      }
   }
   private void imageVerifyCode(HttpServletRequest request, HttpSession session) throws ValidateCodeException {
      String verifyCode = request.getParameter(Constants.VERIFY_CODE);
      String code = (String) session.getAttribute(Constants.VERIFY_CODE);
      if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
         throw new ValidateCodeException("验证码错误, 请重新获取验证码");
      }
   }
}

小白: "为什么你要写imageJSONVerifyCode, imageVerifyCode两个函数? 写一个不就行了?"

小黑: "额, 是的, 把参数改成两个String verifyCode, String code也行"

@Configuration
public class SecurityConfig {
   @Resource
   private AuthenticationConfiguration authenticationConfiguration;
   @Bean
   PasswordEncoder passwordEncoder() {
      return PasswordEncoderFactories.createDelegatingPasswordEncoder();
   }
   @Bean
   public ObjectMapper objectMapper() throws Exception {
      return new ObjectMapper();
   }
   @Bean
   public VerifyCodeFilter verifyCodeFilter() throws Exception {
      VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
      verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
      verifyCodeFilter.setAuthenticationFailureHandler((request, response, exception) -> {
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 401);
         map.put("msg", exception.getMessage());
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      });
      verifyCodeFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 200);
         map.put("msg", "登录成功");
         map.put("user", authentication);
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      });
      return verifyCodeFilter;
   }
   @Bean
   SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
      httpSecurity
            .authorizeHttpRequests()
            .antMatchers(Constants.MATCHERS_LIST)
            .permitAll()
            .anyRequest()
            .authenticated()
      ;
      httpSecurity.formLogin()
            .loginPage(Constants.LOGIN_PAGE)
            .loginProcessingUrl(Constants.LOGIN_PROCESSING_URL)
            .defaultSuccessUrl(Constants.SUCCESS_URL, true)
            .permitAll();
      httpSecurity.logout()
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .logoutSuccessHandler((request, response, authentication) -> {
               HashMap<String, Object> map = new HashMap<>();
               map.put("status", 200);
               map.put("msg", "注销成功");
               map.put("user", authentication);
               response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
               response.getWriter().write(JSONUtil.toJsonStr(map));
            });
      httpSecurity.csrf()
            .disable();
      httpSecurity.addFilterAt(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
      httpSecurity.exceptionHandling()
            .accessDeniedHandler((request, response, accessDeniedException) -> {
               HashMap<String, Object> map = new HashMap<>();
               map.put("status", 401);
               map.put("msg", "您没有权限, 拒绝访问: " + accessDeniedException.getMessage());
//             map.put("msg", "您没有权限, 拒绝访问");
               response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
               response.getWriter().write(JSONUtil.toJsonStr(map));
            })
            .authenticationEntryPoint((request, response, authException) -> {
               HashMap<String, Object> map = new HashMap<>();
               map.put("status", HttpStatus.UNAUTHORIZED.value());
               map.put("msg", "认证失败, 请重新认证: " + authException.getMessage());
//             map.put("msg", "认证失败, 请重新认证");
               response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
               response.getWriter().write(JSONUtil.toJsonStr(map));
            });
      return httpSecurity.build();
   }
}

注意这两行代码, 教你怎么在不使用WebSecurityConfigurerAdapter的情况下拿到AuthenticationManager

@RestController
@Slf4j
public class VerifyCodeController {
   @GetMapping("/verify-code")
   public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception {
      GifCaptcha captcha = CaptchaUtil.createGifCaptcha(Constants.IMAGE_WIDTH, Constants.IMAGE_HEIGHT);
      RandomGenerator randomGenerator = new RandomGenerator(Constants.BASE_STR, Constants.RANDOM_LENGTH);
      captcha.setGenerator(randomGenerator);
      captcha.createCode();
      String code = captcha.getCode();
      session.setAttribute(Constants.VERIFY_CODE, code);
      ServletOutputStream outputStream = response.getOutputStream();
      captcha.write(outputStream);
      outputStream.flush();
      outputStream.close();
   }
}
@Controller
@Slf4j
public class IndexController {
   @GetMapping("login")
   public String login() {
      return "login";
   }
   @GetMapping("")
   @ResponseBody
   public Principal myIndex(Principal principal) {
      return principal;
   }
}

基于认证器方式

public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
   @Resource
   private ObjectMapper objectMapper;
   private final String loginUsername;
   private final String loginPassword;
   public MyDaoAuthenticationProvider(String loginUsername, String loginPassword) {
      this.loginUsername = loginUsername;
      this.loginPassword = loginPassword;
   }
   @SneakyThrows
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      assert requestAttributes != null;
      HttpServletRequest request = requestAttributes.getRequest();
      String contentType = request.getContentType();
      String verifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE);
      if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
         Map map = this.objectMapper.readValue(request.getInputStream(), Map.class);
         String code = (String) map.get(Constants.VERIFY_CODE);
         imageVerifyCode(verifyCode, code);
         String username = (String) map.get(loginUsername);
         String password = (String) map.get(loginPassword);
         UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken
               .unauthenticated(username, password);
         return super.authenticate(authenticationToken);
      }
      String code = request.getParameter(Constants.VERIFY_CODE);
      imageVerifyCode(verifyCode, code);
      return super.authenticate(authentication);
   }
   private void imageVerifyCode(String verifyCode, String code) throws ValidateCodeException {
      if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) {
         throw new ValidateCodeException("验证码错误, 请重新获取验证码");
      }
   }
}
@Slf4j
@Configuration
public class SecurityConfig {
   private static final String NOOP_PASSWORD_PREFIX = "{noop}";
   private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
   @Resource
   private SecurityProperties properties;
   @Bean
   PasswordEncoder passwordEncoder() {
      return PasswordEncoderFactories.createDelegatingPasswordEncoder();
   }
   @Bean
   public ObjectMapper objectMapper() {
      return new ObjectMapper();
   }
   @Bean
   @Lazy
   public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
      SecurityProperties.User user = properties.getUser();
      List<String> roles = user.getRoles();
      return new InMemoryUserDetailsManager(
            User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder()))
                  .roles(StringUtils.toStringArray(roles)).build());
   }
   private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
      String password = user.getPassword();
      if (user.isPasswordGenerated()) {
         log.warn(String.format(
               "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
                     + "Your security configuration must be updated before running your application in "
                     + "production.%n",
               user.getPassword()));
      }
      if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
         return password;
      }
      return NOOP_PASSWORD_PREFIX + password;
   }
   @Bean
   public MyDaoAuthenticationProvider authenticationProvider() throws Exception {
      MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD);
      authenticationProvider.setPasswordEncoder(passwordEncoder());
      authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager());
      return authenticationProvider;
   }
   @Bean
   SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
            .authorizeHttpRequests()
            .antMatchers(Constants.MATCHERS_LIST)
            .permitAll()
            .anyRequest()
            .authenticated()
      ;
      http.formLogin()
            .loginPage(Constants.LOGIN_PAGE)
            .loginProcessingUrl(Constants.LOGIN_PROCESSING_URL)
            .successHandler(new MyAuthenticationSuccessHandler())
            .failureHandler(new MyAuthenticationFailureHandler())
            .permitAll();
      http.logout()
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .logoutSuccessHandler(new MyLogoutSuccessHandler());
      http.csrf()
            .disable();
      http.exceptionHandling(exceptionHandlingConfigurer -> {
         exceptionHandlingConfigurer.authenticationEntryPoint(new MyAuthenticationEntryPoint());
         exceptionHandlingConfigurer.accessDeniedHandler(new MyAccessDeniedHandler());
      })
      ;
      return http.build();
   }
   private static class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 200);
         map.put("msg", "认证成功");
         map.put("user_info", authentication.getPrincipal());
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      }
   }
   private static class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
         log.error("认证失败", exception);
         exception.printStackTrace();
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 401);
         map.put("msg", "认证失败");
         map.put("exception", exception.getMessage());
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      }
   }
   private static class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
      @Override
      public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
         log.error("认证失效", authException);
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", HttpStatus.UNAUTHORIZED.value());
         map.put("msg", "认证失败, 请重新认证: " + authException.getMessage());
//             map.put("msg", "认证失败, 请重新认证");
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      }
   }
   private static class MyAccessDeniedHandler implements AccessDeniedHandler {
      @Override
      public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
         log.error("没有权限", accessDeniedException);
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 401);
         map.put("msg", "您没有权限, 拒绝访问: " + accessDeniedException.getMessage());
//             map.put("msg", "您没有权限, 拒绝访问");
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      }
   }
   private static class MyLogoutSuccessHandler implements LogoutSuccessHandler {
      @Override
      public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         HashMap<String, Object> map = new HashMap<>();
         map.put("status", 200);
         map.put("msg", "注销成功");
         map.put("user", authentication);
         response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
         response.getWriter().write(JSONUtil.toJsonStr(map));
      }
   }
}

以上就是Spring Security实现添加图片验证功能的详细内容,更多关于Spring Security添加图片验证的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringSecurity导致SpringBoot跨域失效的问题解决

    目录 1.CORS 是什么 2.预检请求 3.三种配置的方式 3.1 @CrossOrigin 注解 3.2 实现 WebMvcConfigurer.addCorsMappings 方法 3.3 注入 CorsFilter 4.Spring Security 中的配置 5.这些配置有什么区别 5.1 Filter 与 Interceptor 5.2 WebMvcConfigurer.addCorsMappings 方法做了什么 5.2.1 注入 CORS 配置 5.2.2 获取 CORS 配置

  • SpringSecurity自定义Form表单使用方法讲解

    目录 背景 实验-HttpBasic 实验-自定义登录页面 实验-自定义登录接口 实验-自定义登录数据参数 实验-自定义登录失败.成功处理器 实验-自定义登录成功跳转页面 实验-自定义退出接口 背景 本系列教程,是作为团队内部的培训资料准备的.主要以实验的方式来体验SpringSecurity的各项Feature. 新建一个SpringBoot项目,起名springboot-security-form,核心依赖为Web,SpringSecurity与Thymeleaf. <dependencie

  • Idea中如何查看SpringSecurity各Filter信息

    目录 Filter和Filter Chain Idea Evalute Expression 创建工程 debug启动服务 evaluate expression 总结 Filter和Filter Chain SpringSecurity的认证逻辑是通过Filter Chain实现的,一个项目中Filter是链式执行,其中一环校验不通过,则可终止后续Filter以及Api的调用.     public void doFilter(ServletRequest request, ServletRe

  • 详解Spring Security怎么从数据库加载我们的用户

    目录 本章内容 如何从数据库中读取用户对象? 脱敏为什么没有生效? 总结 本章内容 如何从数据库中读取用户对象 源码分析 如何从数据库中读取用户对象? 1前面我们分析认证的时候就会发现他在DaoAuthenticationProvider中就已经有从数据库中获取用户名和密码的. public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundE

  • SpringBoot Security实现单点登出并清除所有token

    目录 需求 记录token 清除token 解决登出时长过长 需求 A.B.C 系统通过 sso 服务实现登录 A.B.C 系统分别获取 Atoken.Btoken.Ctoken 三个 token 其中某一个系统主动登出后,其他两个系统也登出 至此全部 Atoken.Btoken.Ctoken 失效 记录token pom 文件引入依赖 Redis数据库依赖 hutool:用于解析token <dependency> <groupId>org.springframework.boo

  • Spring Security如何实现升级密码加密方式详解

    目录 本章内容 密码加密方式怎么升级? 升级方案源码 实战 第一种方式: Spring Bean 他是怎么自动升级到BCrypt加密方式的? 第二种方式: 多继承接口方式 第三种方式: HttpSecurity直接添加 本章内容 密码加密方式怎么升级? spring security底层怎么实现的密码加密方式升级? 密码加密方式怎么升级? 前面我们学过DelegatingPasswordEncoder类,但是不清楚他到底是做什么的,我也没讲的很清楚.所以呢,我们就重新再讲一讲它的另一个实际应用.

  • Spring Security实现添加图片验证功能

    目录 本章内容 思路 方案 怎么将字符串变成图片验证码? kaptcha这么玩 hutool这么玩 传统web项目 过滤器方式 认证器方式 总结下 前后端分离项目 基于过滤器方式 基于认证器方式 本章内容 Spring security添加图片验证方式,在互联网上面有很多这种博客,都写的非常的详细了.本篇主要讲一些添加图片验证的思路.还有前后端分离方式,图片验证要怎么去处理? 图片验证的思路 简单的demo 思路 小白: "我们从总体流程上看图片验证在认证的哪一个阶段?" 小黑: &q

  • Spring Security登录添加验证码的实现过程

    登录添加验证码是一个非常常见的需求,网上也有非常成熟的解决方案,其实,要是自己自定义登录实现这个并不难,但是如果需要在 Spring Security 框架中实现这个功能,还得稍费一点功夫,本文就和小伙伴来分享下在 Spring Security 框架中如何添加验证码. 关于 Spring Security 基本配置,这里就不再多说,小伙伴有不懂的可以参考我的书<SpringBoot+Vue全栈开发实战>,本文主要来看如何加入验证码功能. 准备验证码 要有验证码,首先得先准备好验证码,本文采用

  • 详解使用Spring Security进行自动登录验证

    在之前的博客使用SpringMVC创建Web工程并使用SpringSecurity进行权限控制的详细配置方法 中,我们描述了如何配置一个基于SpringMVC.SpringSecurity框架的网站系统.在这篇博客中,我们将继续描述如何使用Spring Security进行登录验证. 总结一下Spring Security的登录验证关键步骤: 1.在数据库中建好三张表,即users.authorities和persistent_logins三个.注意字段的定义,不能少,可以多,名字必须按规定来.

  • spring security实现下次自动登录功能过程解析

    这篇文章主要介绍了spring security实现记住我下次自动登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.原理分析 第一次登陆时,如果用户勾选了readme选项,登陆成功后springsecurity会生成一个cookie返回给浏览器端,浏览器下次访问时如果携带了这个cookie,springsecurity就会放行这次访问. 二.实现方式 2.1 简单实现方式 (1) 在springsecurity的配置文件中,http节

  • Spring Security基于散列加密方案实现自动登录功能

    目录 前言 一. 自动登录简介 1. 为什么要自动登录 2. 自动登录的实现方案 二. 基于散列加密方案实现自动登录 1. 配置加密令牌的key 2. 配置SecurityConfig类 3. 添加测试接口 4. 启动项目测试 三. 散列加密方案实现原理 1. cookie的加密原理分析 2. cookie的解码原理分析 3. 自动登录的源码分析 3.1 令牌生成的源码分析 3.2 令牌解析的源码分析 前言 在前面的2个章节中,一一哥 带大家实现了在Spring Security中添加图形验证码

  • Spring Security 图片验证码功能的实例代码

    验证码逻辑 以前在项目中也做过验证码,生成验证码的代码网上有很多,也有一些第三方的jar包也可以生成漂亮的验证码.验证码逻辑很简单,就是在登录页放一个image标签,src指向一个controller,这个Controller返回把生成的图片以输出流返回给页面,生成图片的同时把图片上的文本放在session,登录的时候带过来输入的验证码,从session中取出,两者对比.这位老师讲的用Spring Security集成验证码,大体思路和我说的一样,但更加规范和通用些. spring securi

  • Spring Security验证流程剖析及自定义验证方法

    Spring Security的本质 Spring Security 本质上是一连串的 Filter , 然后又以一个独立的 Filter 的形式插入到 Filter Chain 里,其名为 FilterChainProxy . 如图所示. 实际上 FilterChainProxy 下面可以有多条 Filter Chain ,来针对不同的URL做验证,而 Filter Chain 中所拥有的 Filter 则会根据定义的服务自动增减.所以无需要显示再定义这些 Filter ,除非想要实现自己的逻

  • Spring Security基于过滤器实现图形验证码功能

    目录 前言 一. 验证码简介 二. 基于过滤器实现图形验证码 1. 实现概述 2. 创建新模块 3. 添加依赖包 4. 创建Producer对象 5. 创建生成验证码的接口 6. 自定义异常 7. 创建拦截验证码的过滤器 8. 编写SecurityConfig 9. 编写测试页面 10. 代码结构 11. 启动项目测试 前言 在前两个章节中,一一哥 带大家学习了Spring Security内部关于认证授权的核心API,以及认证授权的执行流程和底层原理.掌握了这些之后,对于Spring Secu

  • Spring Boot中整合Spring Security并自定义验证代码实例

    最终效果 1.实现页面访问权限限制 2.用户角色区分,并按照角色区分页面权限 3.实现在数据库中存储用户信息以及角色信息 4.自定义验证代码 效果如下: 1.免验证页面 2.登陆页面 在用户未登录时,访问任意有权限要求的页面都会自动跳转到登陆页面. 3.需登陆才能查看的页面 用户登陆后,可以正常访问页面资源,同时可以正确显示用户登录名: 4.用户有角色区分,可以指定部分页面只允许有相应用户角色的人使用 4.1.只有ADMIN觉得用户才能查看的页面(权限不足) 4.2.只有ADMIN觉得用户才能查

  • Spring Security 构建rest服务实现rememberme 记住我功能

    Spring security记住我基本原理: 登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeService,会生成一个token,将token写入到浏览器cookie,同时RememberMeService里边还有个TokenRepository,将token和用户信息写入到数据库中.这样当用户再次访问系统,访问某一个接口时,会经过一个RememberMeAuthenticationFilt

随机推荐