Security的核心之一认证
# Security的核心之一认证
# 基本认证
# 快速上手
要想使用Security,最重要的就是引入对应的jar,下面展示一个简单的例子。创建一个SpringBoot项目,同时引入web模块以及Security模块,对应pom文件如下
<!--Security 框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后再随意编写一个简单接口就能进行测试了
@RestController
public class HelloController {
@RequestMapping("hello")
public String Hello(){
System.out.println();
return "123";
}
}
2
3
4
5
6
7
8
9
之后,当我们运行项目,进行访问/hello
该接口时,你会惊奇的发现,系统自动跳转到了一个登录页面。输入正确的用户名和密码才能继续访问该接口。
正确的用户名为:user,则密码则是在启动项目时,系统随机生成的一个UUID,在日志输出中可以看到
2024-05-02 17:57:51.404 INFO 57556 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 301b4270-4207-40a4-803a-b03afd1e5b19
2
3
至此一个超简单的springsecurity项目就完成了。
# 原理浅解
上述例子,尽管我们只引入了一个SpringBootSecurity的依赖,但SpringBoot却在背后帮我们完成了很多事情:
开启Spring Security自动化配置,开启后,会自动创建一个名为
springSecurityFilterChain
的过滤器,并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecurityFilterChain实际上代理了Spring Security中的过滤链创建一个
UserDetailsService
实例,UserDetailsService 负责提供用户数据,默认的用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串。给用户生成一个默认的登录页面。开启CSRF攻击防御。
开启会话固定攻击防御。
集成X-XSS-Protection。
集成X-Frame-Options以防止单击劫持。
上述例子中,我们对/hello
的请求,会先走一遍SpringSecurity中的过滤器链,并且在最后的FilterSecurityInterceptor过滤器中被拦截下来,因为系统发现用户未认证且该请求不在白名单中。被拦截后,接着会抛出AccessDeniedException异常。
接着异常被某个过滤器(这里是ExceptionTranslationFilter,后问会详细介绍)捕获,它调用某个方法给客户端返回302,要求客户重定向到/login
页面。
# 默认用户
在Spring Security框架中,UserDetails
是一个核心接口,它代表了系统中的用户详细信息,主要用于存储与用户认证和授权相关的数据。这个接口定义了一系列方法来描述用户的安全属性,主要包括:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
2
3
4
5
6
7
8
9
10
11
- getUsername():返回用户的唯一标识,通常是用户名或邮箱。
- getPassword():返回用户的未加密密码。在实际应用中,密码通常会被加密存储,这个方法用于比较用户输入的密码与存储的加密密码。
- getAuthorities():返回一个集合,包含该用户所有的权限(GrantedAuthority对象的集合)。每个GrantedAuthority对象代表一个权限,如
ROLE_ADMIN
、ROLE_USER
等,用于授权判断。 - isAccountNonExpired():指示账户是否未过期。
- isAccountNonLocked():指示账户是否未被锁定。
- isCredentialsNonExpired():指示凭证(如密码)是否未过期。
- isEnabled():指示账户是否启用
综上所述,UserDetail是对用户的定义和规范。
# UserDetails接口设计的原因
设计UserDetails
接口的一个重要目的之一,是为了能够灵活地映射和适应各种系统中原有的用户对象模型或数据库表结构。由于不同的应用可能有不同的用户信息存储方式和数据结构,通过定义这样一个接口,Spring Security允许开发者轻易地将其现有系统的用户数据模型与安全框架对接。
当然还有以下其他原因:
- 标准化:通过定义一个统一的接口,Spring Security确保了所有与用户相关的安全操作都基于相同的用户信息结构,无论这些信息是如何存储或从何处加载的。这提高了代码的一致性和可维护性。
- 灵活性:由于
UserDetails
是一个接口,开发人员可以根据具体应用的需求自由实现它,从而适应不同的用户数据模型和存储方式。这允许了高度的定制性,比如可以包含额外的用户属性、自定义的权限表示等。 - 解耦:通过引入
UserDetailsService
接口,Spring Security将用户信息的检索逻辑与安全框架的其他部分分离,使得用户认证逻辑不必直接关心数据访问细节,实现了业务逻辑与安全逻辑的解耦。 - 安全性:接口强制实现一些关键的安全检查方法,如账户是否过期、是否锁定等,确保了安全最佳实践的遵循,有助于提升系统的整体安全性。
- 集成能力:标准化的接口简化了与其他安全相关组件和服务的集成,比如与LDAP、数据库、OAuth2服务等外部身份验证系统的集成。
- 扩展性:随着应用安全需求的变化,可以通过增加新的方法到自定义的
UserDetails
实现中,轻松扩展用户模型,而无需修改Spring Security框架本身。
实际上,开发人员可以通过创建自定义的UserDetails
实现类,将系统中用户表的字段映射到UserDetails
接口规定的方法中,比如将数据库中的用户名、密码、角色等信息对应到getUsername()
、getPassword()
、getAuthorities()
等方法。这样,即使系统原先的用户表结构与Spring Security的标准接口不完全吻合,也能通过实现该接口来无缝整合,确保应用可以利用Spring Security的强大安全功能,而不必对原有用户数据模型做大幅改动。因此,UserDetails
接口的设计极大增强了Spring Security的通用性和适用范围。
# 验证用户UserDetailsService
在解释设计UserDetails
原因时,提到了另一个接口UserDetailsService
。在Security框架中,它是处理用户认证的核心组件之一。这个接口的设计主要是为了根据用户名加载用户的详细信息,进而实现对用户身份的验证。该接口非常简单,它只有一个方法:
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
2
3
loadUserByUsername: 此方法接收一个字符串参数(通常是用户名或邮箱),然后返回一个实现了UserDetails
接口的对象。这个方法的主要职责是从数据库、LDAP服务器、内存或其他数据源中加载用户的详细信息,包括但不限于用户名、密码、权限等。如果找不到对应的用户,那么方法应该抛出UsernameNotFoundException
异常。
在实际开发中,开发者通常需要根据实际项目的需求,实现UserDetailsService
接口(自定义实现),定义具体的用户信息加载逻辑。这通常涉及到与数据库的交互,使用JDBC、JPA、Hibernate或其他ORM工具从数据库中查询用户信息。
如果开发者没有自定义UserDetailsService
的实现,Spring Security提供了几种现成的UserDetailsService
实现,例如InMemoryUserDetailsManager
(用于内存中存储用户信息,适合于测试和演示)和JdbcUserDetailsManager
(用于从数据库中加载用户信息)。如果仅仅只是引入Security依赖,则默认使用的就是InMemoryUserDetailsManager
.
UserDetailsService
的灵活性和可扩展性,使得Spring Security能够适应多种用户认证场景,无论是简单的内存认证还是复杂的数据库认证。通过自定义实现,开发者可以精确控制用户认证逻辑,实现与现有用户管理体系的无缝集成,是Spring Security强大认证机制的基础。
# 认证流程
在Spring Security的认证流程中,当用户尝试登录时,框架会调用UserDetailsService
的loadUserByUsername
方法来获取用户信息。随后,框架会使用这些信息与用户提交的凭据(如密码)进行比较,以验证用户身份。认证成功后,用户的详细信息会被封装成一个Authentication
对象,并存储在安全上下文中,用于后续的授权决策。
# 重定义用户名及密码
针对UserDetailsService
的自动配置类是UserDetailsServiceAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class},
type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector"}
)
public class UserDetailsServiceAutoConfiguration {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
public UserDetailsServiceAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
)
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
}
private String getOrDeducePassword(User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
它的头部,加了一些注解,我们一起来看看
@Configuration(proxyBeanMethods = false)
@Configuration
:标记该类为一个配置类,其中可以包含@Bean
定义的方法,用于向Spring容器注册Bean。proxyBeanMethods = false
:这是一个Spring Boot 2.2引入的特性,表示配置类中的Bean方法不会被代理以进行缓存或事务管理。当设置为false
时,每个Bean方法都会被独立调用,这对于那些依赖于运行时状态或需要每次调用都产生新实例的场景非常有用。
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnClass
:这是一个条件注解,表示只有当指定的类(这里是AuthenticationManager.class
)在类路径上存在时,这个配置类才会被处理和注册。这是一种延迟初始化的策略,有助于减少不必要的初始化工作。
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnBean
:同样是一个条件注解,确保在容器中已经存在指定类型的Bean(这里是ObjectPostProcessor.class
)时,当前配置类才会生效。这通常用来确保依赖的Bean已经准备好。
@ConditionalOnMissingBean(...)
@ConditionalOnMissingBean
:这个注解保证,只有当应用上下文中缺少指定类型的Bean时,该配置类才会被注册。这里有两个条件组合:
- 类型为
AuthenticationManager.class
,AuthenticationProvider.class
,UserDetailsService.class
的Bean不存在。 - 类型为
org.springframework.security.oauth2.jwt.JwtDecoder
和org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector
的Bean也不存在。
这意味着,如果这些Bean中的任何一个已经在容器中定义,该自动配置类将不会创建它们的默认实现,以免造成冲突。
- 类型为
# SecurityProperties
默认情况中,上面的情况都满足,于是InMemoryUserDetailsManager
提供了一个它的实例(Bean)。且它的方法中我们发现了跟user有关的信息,再进入SecurityProperties
类,我们大概就会明白默认用户名、密码是如何来的了,
// 部分源码
public SecurityProperties.User getUser() {
return this.user;
}
public SecurityProperties.Filter getFilter() {
return this.filter;
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList();
private boolean passwordGenerated = true;
public User() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
另外,当看了SecurityProperties的源码后,只要对Spring Boot中properties 属性的加载机 制有一点了解,就会明白,只要我们在项目的application.properties 配置文件中添加如下配置,就能定制 SecurityProperties.User类中各属性的值:
spring:
security:
user:
name: sugar
password: 123
roles: admin,user
2
3
4
5
6
除了默认的登录页,还有默认的登出页,在浏览器中输入http://localhost:8080/logout 就可以看到注销登录页面
# 自定义登录配置configure
在实际开发中,用户登录页应该是开发者自定义的而不是像上述例子中那样简单,且登录后的错误提示、或者登录成功后跳转的页面、登录是否需要校验码等这些都应该是自定义的。
下面就一起来看看如何对这些登录中的参数进行配置(在日常开发中关于登录的各种需求会慢慢展开)。实现一个Security的配置类,并继承WebSecurityConfigurerAdapter
类。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启权限配置
http.authorizeRequests()
//所有的请求 都要认证之后
.anyRequest().authenticated()
//用and来表示配置过滤器结束,以便进行下一个过滤器的创建和配置
.and()
//开启表单登录配置
.formLogin().loginPage("/login.html")
//登录接口地址
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
//表示跟 登录相关的接口和地址 都不拦截
.permitAll()
.and()
//禁用csrf功能
.csrf().disable();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
每个过滤器用.and()进行分割
# 登录跳转
当用户登录成功之后,除了defaultSuccessUrl 方法可以实现登录成功后的跳转之外,successForwardUrl 也可以实现登录成功后的跳转,代码如下:
// 替换成第二行代码
.defaultSuccessUrl("/index")
.successForwardUrl("/index")
// 也不会考虑未登录前 访问的地址
.defaultSuccessUrl("/index",true)
2
3
4
5
6
虽然他们都能实现一样的功能,但名称不同,应该也有一些不同的地方吧。确实如此,defaultSuccessUrl
和successForwardUrl
的区别如下:
- defaultSuccessUrl表示当用户登录成功之后,会自动重定向到登录之前的地址上,如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到defaultSuccessUrl指定的页面中。例如,用户在未认证的情况下,访问了/hello页面,此时会自动重定向到登录页面,当用户登录成功后,就会自动重定向到/hello页面;而用户如果一开始就访问登录页面,则登录成功后就会自动重定向到defaultSuccessUrl所指定的页面中。
- successForwardUrl则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到successForwardUrl所指定的页面。
- defaultSuccessUrl 有一个重载方法,如果重载方法的第二个参数传入 true,则defaultSuccessUrl的效果与successForwardUrl类似,即不考虑用户之前的访问地址,只要登录成功,就重定向到defaultSuccessUrl所指定的页面。不同之处在于, defaultSuccessUrl是通过重定向实现的跳转(客户端跳转) ,而successForwardUrl则是通过服务器端跳转实现的。
# AuthenticationSuccessHandler
无论是defaultSuccessUrl还是successForwardUrl,最终所配置的都是AuthenticationSuccessHandler
接口的实例。AuthenticationSuccessHandler 默认的三个实现类,无论是哪一个,都是用来处理页面跳转 的。这里就不展开了,因为现如今,登录后并不需要进行页面跳转。
当下流行的前后端分离开发中,用户登录成功后,无需页面跳转,只需要给前端返回一个JSON数据即可,告诉前端登录成功还是登录失败,前端收到消息之后自行处理。像这样的需求,我们可以通过自定义AuthenticationSuccessHandler
的实现类来完成。
实现自定义类后,最后在SecurityConfig配置类中对successHandler
方法传入自定义的AuthenticationSuccessHandler的实例对象即可。
//登录成功后 实现自定义处理逻辑
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "登录成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
// 接着在 刚刚的配置中进行 对应配置,如此依赖成功后就不再是页面跳转
.loginProcessingUrl("/doLogin")
//.defaultSuccessUrl("/index")
// 处理成功登录逻辑
.successHandler(new MyAuthenticationSuccessHandler())
.failureUrl("/login.html")
.usernameParameter("uname")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# AuthenticationFailureHandler
既然有自定义登录成功的处理逻辑,也应该能够自定义登录失败的处理逻辑。是的,它对应的方法是failureHandler()
,需要传入的参数类型是AuthenticationFailureHandler
,它是一个接口,规定用来处理登录失败逻辑的,它的默认实现类有5个,分别是SimpleUrlAuthenticationFailureHandler 、ExceptionMappingAuthenticationFailureHandler、ForwardAuthenticationFailureHandler 、AuthenticationEntryPointFailureHandler、DelegatingAuthenticationFailureHandler 这里就不展开讲了,后续有需要会提到。
了解了上述的情况,所以如果我们要自定义登录失败的处理逻辑,就应该创建一个类并实现AuthenticationFailureHandler
接口。
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 500);
resp.put("msg", "登录失败!" + exception.getMessage());
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
接着在配置类中进行配置
//.failureUrl("/login.html")
.failureHandler(new MyAuthenticationFailureHandler())
.usernameParameter("uname")
.passwordParameter("passwd")
2
3
4
至此配置完成后,当用户再次登录失败,就不会再进行页面跳转了,而是直接返回JSON字符串。
# 注意
以上就是两种认证方式:基于内存的认证(默认的即为内存认证)以及表单认证。
在前后端分离中,我们通常是不需要定义登录页的,所以我们要把登录请求排除在认证之外,否则就会陷入无限循环——进行登录请求,发现没有登录,于是请求用户登录,之后又是一个登录请求....
除了登录请求需要排除在认证之外,还有一些其他请求需要排除,比如:忘记密码请求、验证码请求等。当然还可以更多,一切根据你的需求来。对应代码如下:
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
2
3
4
5
6
7
8
9
10
SpringSecurity还支持HTTP摘要认证,这里就不展开了
# 未登录时
一般在我们没有认证时,会直接重定向到登录页面。但是在前后端分离中,这个逻辑是有问题的,因为此时,后端没有页面,未认证时也没办法直接重定向到登录页面啊!
因此这种情况,它也应该是返回一段提示信息给前端。当然,SpringSecurity也是支持的,并有对应的api提供。
未认证时,同样有个专门的方法来处理,即authenticationEntryPoint()
方法,这个方法中需要一个参数LoginUrlAuthenticationEntryPoint
,在LoginUrlAuthenticationEntryPoint类中有一个方法 commence()
。
它的配置方法跟上面2个类似:
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
在配置类中进行配置
// 部分配置
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
// 静态资源,可匿名访问
//禁用csrf功能
.csrf().disable()
//未登录时异常处理
.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint());
2
3
4
5
6
7
8
9
至此,未登录时处理完成。
# 注销登录
Spring Security 中提供了默认的注销页面,且默认的路径为:项目地址/logout,当然开发者也可以根据自己的需求对注销登录进行定制。同样是在继承了WebSecurityConfigurerAdapter的配置类中进行配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启权限配置
http.authorizeRequests()
//所有的请求 都要认证之后
//用and来表示配置过滤器结束,以便进行下一个过滤器的创建和配置
.anyRequest().authenticated().and()
//开启表单登录配置
.formLogin()
// ....隐藏了部分表单设置
.permitAll()
.and()
// 开始定制 注销页面
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/logout.html")
.and()
//禁用csrf功能
.csrf().disable();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 通过 logout()方法开启注销登录配置。
- logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout
- invalidateHttpSession 表示是否使session失效,默认为true
- clearAuthentication 表示是否清除认证信息,默认为true
- logoutSuccessUrl表示注销登录后的跳转地址。
配置完成后,启动项目,登录成功后,在浏览器中访问:项目地址/logout就可以发起注销登录请求了。并且注销成功后,会自动跳转到logout.html页面。
如果项目有需要,开发者也可以配置多个注销登录的请求,同时还可以指定请求的方法:
//.logoutUrl("/logout")
.logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET")
,new AntPathRequestMatcher("/logout2","POST")
))
2
3
4
不过实际开发中,我们常用的仍是只返回对应信息给前端(前后端分离模式),它的定义跟之前的AuthenticationSuccessHandler
、AuthenticationFailureHandler
类似,都是需要实现一个接口并实现其中的方法。最后在配置类中调用对应方法。
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> resp = new HashMap<>();
resp.put("status", 200);
resp.put("msg", "用户成功登出!" + authentication.getName());
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
最后在配置类中调用logoutSuccessHandler()
,进行调用
.invalidateHttpSession(true)
.clearAuthentication(true)
//.logoutSuccessUrl("/logout.html")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
2
3
4
5
至此,注销登录且只返回信息完成。