Spring Secuirity

时间:2023-01-10 20:56:24

简介:

Spring Secuirity 是Spring家族中的一个安全管理框架。相比于另一个框架shiro,他提供了更加丰富的功能,社区资源也比市容丰富。

一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity, Shiro的上手更加的简单。

—般Web应用的需要进行认证和授权。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能。

整体架构

在的架构设计中,认证授权是分开的,无论使用什么样的认证方式。都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是可以非常方便地整合一些外部的解决方案。

Spring Secuirity

认证

AuthenticationManager

在Spring Security中认证是由AuthenticationManager接口来负责的,接口定义为:

Spring Secuirity

  • 返回 Authentication 表示认证成功
  • 返回 AuthenticationException 异常,表示认证失败。

AuthenticationManager 主要实现类为 ProviderManager,在 ProviderManager 中管理了众多 AuthenticationProvider 实例。在一次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider ,用来实现多种认证方式,这些 AuthenticationProvider 都是由 ProviderManager 进行统一管理的。

Spring Secuirity

Authentication

认证以及认证成功的信息主要是由 Authentication 的实现类进行保存的,其接口定义为:

Spring Secuirity

  • getAuthorities 获取用户权限信息
  • getCredentials 获取用户凭证信息,一般指密码
  • getDetails 获取用户详细信息
  • getPrincipal 获取用户身份信息,用户名、用户对象等
  • isAuthenticated 用户是否认证成功

SecurityContextHolder

SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。

总结:

AuthenticationManager被ProviderManager实现,用来管理AuthenticationProvider 实例,这个实例包含很多认证方式,如短信,表单等等。入参是只含有用户名和密码的Authentication,返回数据库中包含的该用户的所有信息。

Authentication保存认证者的信息。

SecurityContextHolder用来获取用户登录后的信息。注意点是讲用户信息进行线程绑定。在使用时将用户信息绑定到当前线程的SecurityContextHolder中,使用完毕清除当前线程的SecurityContextHolder同时将用户信息放到session中,方便下次使用。

授权

当完成认证后,接下米就是授权了。在 Spring Security 的授权体系中,有两个关键接口,

AccessDecisionManager

AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。

Spring Secuirity

AccessDecisionVoter

AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。

Spring Secuirity

AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。

ConfigAttribute

ConfigAttribute,用来保存授权时的角色信息

Spring Secuirity

在 Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具各的角色和请求某个 资源所需的 ConfigAtuibute 之间的关系。

总结:

AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。

AccessDecisionVoter (访问决定投票器),比较用户所具各的角色和请求某个资源所需的 ConfigAtuibute 之间的关系,具备访问权限则投出赞成票。

ConfigAttribute,用来保存授权时的角色信息。

环境搭建

创建SpringBoot工程

引入SpringSecurity

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

引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。

必须登陆之后才能对接口进行访问。

实现原理

​https://docs.spring.io/spring-security/site/docs/5.5.4/reference/html5/#servlet-architecture​

SpringBootWebSecurityConfiguration

Spring Secuirity

对anyRequest()都进行拦截

默认生效条件

Spring Secuirity

  • 条件一 classpath中存在 SecurityFilterChain.class, HttpSecurity.class
  • 条件二 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class

总结:

只要我们不自定义配置类(比如WebSecurityConfigurerAdapter) ,条件都是满足的,也就加载默认的配置。否则如果要进行自定义配置,就要继承这个WebSecurityConfigurerAdapter类,通过覆盖类中方法达到修改默认配置的目的。

Spring Secuirity

执行流程(即SecurityFilter的使用情况和调用顺序)

在 SpringSecurity 中 认证、授权 等功能都是基于​​过滤器​​完成的。

Spring Secuirity

需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个 FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中。FilterChainProxy 作为一个顶层的管理者,将统一管理 Security Filter。FilterChainProxy 本身是通过 Spring 框架提供的 DelegatingFilterProxy 整合到原生的过滤器链中。

SecurityFilter的使用情况和调用顺序如下:

过滤器

过滤器作用

默认是否加载

ChannelProcessingFilter

过滤请求协议 HTTP 、HTTPS

NO

WebAsyncManagerIntegrationFilter

将 WebAsyncManger 与 SpringSecurity 上下文进行集成

YES

SecurityContextPersistenceFilter

在处理请求之前,将安全信息加载到 SecurityContextHolder 中

YES

HeaderWriterFilter

处理头信息加入响应中

YES

CorsFilter

处理跨域问题

NO

CsrfFilter

处理 CSRF

YES

LogoutFilter

处理注销登录

YES

OAuth2AuthorizationRequestRedirectFilter

处理 OAuth2 认证重定向

NO

Saml2WebSsoAuthenticationRequestFilter

处理 SAML 认证

NO

X509AuthenticationFilter

处理 X509 认证

NO

AbstractPreAuthenticatedProcessingFilter

处理预认证问题

NO

CasAuthenticationFilter

处理 CAS 单点登录

NO

OAuth2LoginAuthenticationFilter

处理 OAuth2 认证

NO

Saml2WebSsoAuthenticationFilter

处理 SAML 认证

NO

UsernamePasswordAuthenticationFilter

处理表单登录

YES

OpenIDAuthenticationFilter

处理 OpenID 认证

NO

DefaultLoginPageGeneratingFilter

配置默认登录页面

YES

DefaultLogoutPageGeneratingFilter

配置默认注销页面

YES

ConcurrentSessionFilter

处理 Session 有效期

NO

DigestAuthenticationFilter

处理 HTTP 摘要认证

NO

BearerTokenAuthenticationFilter

处理 OAuth2 认证的 Access Token

NO

BasicAuthenticationFilter

处理 HttpBasic 登录

YES

RequestCacheAwareFilter

处理请求缓存

YES

SecurityContextHolder<br />AwareRequestFilter

包装原始请求

YES

JaasApiIntegrationFilter

处理 JAAS 认证

NO

RememberMeAuthenticationFilter

处理 RememberMe 登录

NO

AnonymousAuthenticationFilter

配置匿名认证

YES

OAuth2AuthorizationCodeGrantFilter

处理OAuth2认证中授权码

NO

SessionManagementFilter

处理 session 并发问题

YES

ExceptionTranslationFilter

处理认证/授权中的异常

YES

FilterSecurityInterceptor

处理授权相关

YES

SwitchUserFilter

处理账户切换

NO

可以看出,Spring Security 提供了 30 多个过滤器。默认情况下Spring Boot 在对 Spring Security 进入自动化配置时,会创建一个名为 SpringSecurityFilerChain 的过滤器,并注入到 Spring 容器中,这个过滤器将负责所有的安全管理,包括用户认证、授权、重定向到登录页面等。具体可以参考WebSecurityConfiguration的源码:

Spring Secuirity

Spring Secuirity

自定义

自定义资源认证规则

前面提到,默认认证规则生效需要满足两个条件

  • 条件一 classpath中存在 SecurityFilterChain.class, HttpSecurity.class
  • 条件二 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class

条件一当我们启动springboot项目时就已经生成,无法修改。所以我们可以通过自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class两个类来实现资源认证规则的自定义。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and().formLogin();
}
}

对于index放行,对于其他的页面采取认证表单认证。

总结:

要实现自定义的认证规则,建议实现WebSecurityConfigurerAdapter类,重写configure方法。

自定义登陆界面

在WebSecurityConfigurerAdapter的实现类中指定跳转的路径、页面,以及前端会被拦截器捕捉到的请求。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/login").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/dologin")
.usernameParameter("username")
.passwordParameter("password")//修改后端接收前端传来的用户名和密码
.successForwardUrl("/index") //认证成功之后调转的路径 forward跳转 跳转之后url不变
//.defaultSuccessUrl("/index") 认证成功之后跳转,但是这个跳转是重定向,跳转之后路径改变 上一次保存了请i去就优先跳转你的请求界面
.failureUrl("/login") //redirect跳转 错误信息存储在session中
.failureForwardUrl("/login") //forward跳转 错误信息放在request中 thymeleaf默认拿到的就是request
.and()
.csrf().disable();//禁用csrf跨站请求保护
}
}

定义跳转的controller以及页面

@Controller
public class LoginController {
@RequestMapping("/login")
public String login(){
return "login";
}
}
<!DOCTYPE html >
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>login</title>
<form method="post" th:action="@{/dologin}">
username:<input name="username" type="text">
password:<input name="password" type="password">
<input type="submit" value="submit">
</form>
</head>
<body>

</body>
</html>

总结:

通过自定义configure方法中的对应的方法来指定我们需要修改的认证页面即可达到自定义认证界面的目的。

前后端分离自定义认证成功的默认行为

对于前后端分离的情况,前面提到的.successForwardUrl("/index") 和 .defaultSuccessUrl("/index")就不再试用了,我们更期待的是后端在认证成功之后像前端传递一些数据,前端再根据这些数据或者信息进行渲染。这个时候就有另外一个方法.successHandler(new MyAuthenticationSuccessHandler())。

Spring Secuirity

需要传递AuthenticationSuccessHandler对象,所以我们可以通过实现这个类来传递需要传递的信息。

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> result = new HashMap<>();
result.put("msg","success");
result.put("code",200);
result.put("Authentication",authentication);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s) ;
}
}

此时再进行测试我们发现他就不是跳转页面了而是展示了我们在重写的方法中打印的数据。

Spring Secuirity

前后端分离自定义认证失败的默认行为

和成功差不多,就是要实现AuthenticationFailureHandler接口 。调用failureHandler(传AuthenticationFailureHandler接口实现类)。

总结:

成功:successHandler(new MyAuthenticationSuccessHandler())。

失败:failureHandler(new MyAuthenticationFailureHandler())。


注销登录(前后端不分离)

注销登录默认开启。需要在地址栏以get的方式访问/logout。

对于传统前后端不分离的方式,可以直接返回登录页面。

.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/aa","GET"),//指定多个退出登录的路径,以及方式
new AntPathRequestMatcher("/bb","POST")
))
.invalidateHttpSession(true)
.clearAuthentication(true)
@Controller
public class logoutController {
@RequestMapping("/logout")
public String logout(){
return "logout";
}
}
<!DOCTYPE html >
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>logout</title>
<form method="post" th:action="@{/bb}">
<input type="submit" value="logout">
</form>
</head>
<body>

</body>
</html>

注销登录(前后端分离)

调用.logoutSuccessHandler(new MyLogoutSuccessHandler())方法,传入自定义的AuthenticationSuccessHandler的实现类。

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> result = new HashMap<>();
result.put("msg","success");
result.put("code",200);
result.put("Authentication",authentication);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s) ;
}
}

Spring Secuirity

获取用户认证信息

SpringContextHolder

Spring Security 会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到SecurityContextHolder中。

SecurityContextHolder中的数据保存默认是通过ThreadLocal 来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContexHolder中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session中取出用户登录数据,保存SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。

实际上securityContextHolder中存储是SecurityContext,在SecurityContext中存储是Authentication。

Spring Secuirity

存放策略

Spring Secuirity

  • MODE THREADLOCAL︰
  • 这种存放策略是将SecurityContext存放在ThreadLocal中,大家
    知道Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达 Servlet,都是由一个线程来处理的。这也是SecurityContextHolder的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
  • MODE INHERITABLETHREADLOCAL∶
  • 这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  • MODE GLOBAL︰
  • 这种存储模式实际上是将数据保存在一个静态变量中,在JavaWeb开发中,这种模式很少使用到。

SecurityContextHolderStrategy通过SecurityContextHolder可以得知,SecurityContextHolderStrategy接口用来定义存储策略方法。所以我们想要使用不同的存储策略只需要在java虚拟机运行环境上配置属性-Dspring.security.strategy=策略即可。

自定义认证数据源

认证流程

Spring Secuirity

三者关系

Spring Secuirity

AuthenticationManager与ProviderManager

Spring Secuirity

Spring Secuirity

弄清楚认证原理之后我们来看下具体认证时数据源的获取。默认情况下

AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过 UserDetailsService 完成数据源的校验。他们之间调用关系如下:

Spring Secuirity

总结:

AuthenticationManager是认证管理器,在Spring Security中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManger进行实现。每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用UserDetailService来实现。


定义内存数据源

//要使用默认的,直接自定义一个返回UserDetailsService的Bean
@Bean
public UserDetailsService userDetailsService(){
//定义基于内存的数据源
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("ZMY").password("{noop}123").roles("admin").build());
return userDetailsManager;
}

//springboot对security的默认配置,在Bean工厂中创建AuthenticationManager
// @Autowired
// public void initalize(AuthenticationManagerBuilder builder) throws Exception {
// //builder.userDetailsService(manager);
// }


//自定义AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
auth.userDetailsService(userDetailsService());
}

总结:

默认

1.默认自动配置创建全局AuthenticationManager默认找当前项目中是否存在自定义UserDetailService实例自动将当前项目UserDetailService实例设置为数据源

2.默认自动配置创建全局AuthenticationManager在工厂中使用时直接在代码中注入即可


自定义

1.一旦通过configure方法自定义AuthenticationManager实现就回将工厂中自动配AuthenticationManager进行覆盖

2.一旦通过configure方法自定义AuthenticationManager实现需要在实现中指定认证数据源对象UserDetaiService实例

3.一旦通过configure方法自定义AuthenticationManager实现,这种方式创建的AuthenticationManager对象只存在于工厂内部,即不可以通过@Autowired注解在别的地方注入。想要在工厂中暴露这个实例就需要实现以下方法:

//自定义AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
auth.userDetailsService(userDetailsService());
}

//重写此方法可以将自定义的AuthenticationManager暴露在Bean工厂中,从而实现任何地方的注入
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

定义数据库数据源

依赖

数据库及类

配置jdbc、mybatis等等

spring.thymeleaf.cache=false
spring.thymeleaf.check-template-location=false


spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security?characterEncoding=UTF-8&useSSl=false
spring.datasource.username=root
spring.datasource.password=Tx110304

#mybatis配置
mybatis.mapper-locations=classpath:/com/zmy/mapper/UserDaoMapper.xml
mybatis.type-aliases-package=com.zmy.springsecurity.entity

#日志处理
logging.level.com.zmy.springsecurity=debug

UserDao

@Mapper
public interface UserDao {
User loadUserByUsername(String username);
List<Role> getRolesByUid(Integer uid);
}
@Component
public class MyUserDetailService implements UserDetailsService {

@Autowired
private UserDao userDao;

// @Autowired
// public MyUserDetailService(UserDao userDao) {
// this.userDao = userDao;
// }

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
User user = userDao.loadUserByUsername(username);
if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("No Such User");
//查询权限信息
List<Role> roles = userDao.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}

UserDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zmy.springsecurity.dao.UserDao">
<!--根据用户名查询用户方法-->
<select id="loadUserByUsername" resultType="com.zmy.springsecurity.entity.User" >
select id,
username,
password,
enabled,
accountNonExpired,
accountNonLocked,
credentialsNonExpired
from user
where username = #{username}
</select>

<!--根据用户id查询角色信息-->
<select id="getRolesByUid" resultType="com.zmy.springsecurity.entity.Role">
select r.id,
r.name,
r.name_zh nameZh
from role r,user_role ur
where r.id = ur.rid and ur.uid = #{uid}
</select>


</mapper>

WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//bean工厂中注入自定义的UserDetailsService的实现类
@Autowired
private MyUserDetailService myUserDetailService;
//自定义AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
//指定使用我们自定义的service。
auth.userDetailsService(myUserDetailService);
}
}

总结:

基于数据库的数据源主要还是实现UserDetailsService接口,重写loadUserByUsername方法。一旦我们实现了这个接口并且重写了对应的方法,通过configure(AuthenticationManagerBuilder auth)方法指定了userDetailsService后,security在认证的时候就会调用我们重写的loadUserByUsername方法去数据库中拿数据。

验证码

使用google的kapacha。

依赖

<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

配置类

@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha(){
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;
}
}

处理验证码生成请求的controller

@Controller
public class VerifyCodeController {
@Autowired
Producer producer;

@RequestMapping("/vc.jpg")
public void vc(HttpServletResponse httpServletResponse, HttpSession httpSession) throws IOException {
//生成验证码
String verifyCode = producer.createText();
//存储验证码(session或者redis)
httpSession.setAttribute("kaptcha",verifyCode);
//生成验证码图片
BufferedImage image = producer.createImage(verifyCode);
//响应图片
httpServletResponse.setContentType("image/jpg");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
ImageIO.write(image,"jpg",outputStream);
}
}

注意:要在自定义的SecurityConfig里面放行生成验证码的请求路径。

Spring Secuirity

待更新。。。