乐趣区

关于spring-security:Spring-Security怎么添加图片验证功能

前言

Spring security 增加图片验证形式,在互联网下面有很多这种博客,都写的十分的具体了。本篇次要讲一些增加图片验证的思路。还有前后端拆散形式,图片验证要怎么去解决?

本章内容

  1. 图片验证的思路
  2. 简略的 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

小黑: “ 默认这个函数须要 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));
      }
   }
}
退出移动版