本篇目录:
一、默认情况
二、自定义用户认证
三、自定义用户登录页面
四、自定义登录成功、失败处理
五、图形验证码
六、记住我功能
七、Session管理
八、退出操作
首先说明本文所用的SpringSecurity版本是2.0.4.RELEASE。下面逐个功能介绍。
一、默认情况
1、构建与配置
1)pom.xml
org.springframework.boot
spring-boot-starter-security
2)application.properties
无需配置
3)UserController.java
@GetMapping("/user")public Listquery(){
List users = new ArrayList();
users.add(new User("1","张三","123456",newDate()));returnusers;
}
2、启动与测试
1)启动程序,控制台打印出默认密码:“Using generated security password: 15a189e8-accb-407a-ad81-2283c8b3bdbf”
2)浏览器输入:http://localhost:8080/user,跳转到表单登录页面
3)输入默认用户名user与默认用户密码15a189e8-accb-407a-ad81-2283c8b3bdbf,访问到数据
二、 自定义用户认证
1、实现UserDetailsService接口
@Componentpublic class MyUserDetailsService implementsUserDetailsService{
@AutowiredprivatePasswordEncoder passwordEncoder;
@Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {return new User(username, passwordEncoder.encode("123456"),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
@BeanpublicPasswordEncoder passwordEncoder() {return newBCryptPasswordEncoder();
}
}
2、说明
1)认证时用户名任意,密码是12345,输错密码,提示坏的凭证。
2)必须加密,实际项目中,将密码passwordEncoder.encode("123456")进行加密,写入数据库。
3)构造函数四个true假如为false依次代表:用户已失效;用户帐号已过期;用户凭证已过期;用户帐号已被锁定。
return new User(username, passwordEncoder.encode("123456"),true,true,true,true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
三、自定义用户登录页面
登录页面/static/login.html
登录标准登录页面
表单登录
用户名 | |
密码 | |
登录 |
1、loginPage指定登录页面
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.formLogin()
.loginPage("/login.html")//指定登录页面
.loginProcessingUrl("/authentication/form");//指定登录页面中表单的url
http.authorizeRequests()
.antMatchers("/login.html").permitAll()//该路径不需要身份认证
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
}
2、loginPage指定Controller,自定义判断
1)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.formLogin()
.loginPage("/authentication/require")//指定需要认证时路径
.loginProcessingUrl("/authentication/form");//指定登录页面中表单的url
http.authorizeRequests()
.antMatchers("/login.html").permitAll()//该路径不需要身份认证
.antMatchers("/authentication/require").permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
}
2)SecurityController.java
@RestControllerpublic classSecurityController {private RequestCache requestCache=newHttpSessionRequestCache();private RedirectStrategy redirectStrategy=newDefaultRedirectStrategy();
@RequestMapping("/authentication/require")
@ResponseStatus(code=HttpStatus.UNAUTHORIZED)public SimpleResponse requireAuthentication(HttpServletRequest request,HttpServletResponse response) throwsException {
SavedRequest savedRequest=requestCache.getRequest(request, response);if(savedRequest!=null) {
String targetUrl=savedRequest.getRedirectUrl();
System.out.println("引发跳转的请求是:"+targetUrl);if(StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response,"/login.html");
}
}return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
}
}
3)index.html
indexindex测试页面
测试输入http://localhost:8080/index.html,跳转到登录页,输入用户名、密码跳转到index.html页面。
测试输入http://localhost:8080/user,页面打印出{"content":"访问的服务需要身份认证,请引导用户到登录页"}。
四、自定义登录成功、失败处理
1、构建与配置
1)pom.xml添加处理json依赖
org.codehaus.jackson
jackson-mapper-asl
1.9.13
2)AuthenticationSuccessHandler.java
@Component("authenticationSuccessHandler")public class AuthenticationSuccessHandler extendsSavedRequestAwareAuthenticationSuccessHandler{
@AutowiredprivateSecurityProperties securityProperties;private ObjectMapper objectMapper=newObjectMapper();
@Overridepublic voidonAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)throwsIOException, ServletException {
System.out.println("登录成功!");if(LoginResponseType.JSON.equals(securityProperties.getLoginType())) {//如果配置了JSON格式,返回如下信息
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else {//否则执行默认的方式,跳转原请求地址
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
3)AuthenticationFailureHandler.java
@Component("authenticationFailureHandler")public class AuthenticationFailureHandler extendsSimpleUrlAuthenticationFailureHandler {
@AutowiredprivateSecurityProperties securityProperties;private ObjectMapper objectMapper=newObjectMapper();
@Overridepublic voidonAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception)throwsIOException, ServletException {
System.out.println("登录失败!");if(LoginResponseType.JSON.equals(securityProperties.getLoginType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(newSimpleResponse(exception.getMessage())));
}else{//执行默认的方式,跳转loginPage配置的地址super.onAuthenticationFailure(request, response, exception);
}
}
}
4)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
privateAuthenticationFailureHandler authenticationFailureHandler;
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.formLogin()
.loginPage("/authentication/require")//指定需要认证时路径
.loginProcessingUrl("/authentication/form")//指定登录页面中表单的url
.successHandler(authenticationSuccessHandler)//认证成功后自定义处理逻辑
.failureHandler(authenticationFailureHandler);//认证失败后自定义处理逻辑
http.authorizeRequests()
.antMatchers("/login.html").permitAll()//该路径不需要身份认证
.antMatchers("/authentication/require").permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
}
5)application.properties
project.security.loginType=REDIRECT
6)SecurityCoreConfig.java
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)public classSecurityCoreConfig {
}
7)LoginResponseType.java
public enumLoginResponseType {
REDIRECT,
JSON
}
8)SecurityProperties.java
@ConfigurationProperties(prefix="project.security")public classSecurityProperties {private LoginResponseType loginType=LoginResponseType.JSON;//默认JSON
publicLoginResponseType getLoginType() {returnloginType;
}public voidsetLoginType(LoginResponseType loginType) {this.loginType =loginType;
}
}
2、测试
1)application.properties中配置REDIRECT,输入localhost:8080/index.html,控制台打印如下信息,并跳转登录页
1.1)输入错误密码,控制台打印如下信息,浏览器显示:{"content":"访问的服务需要身份认证,请引导用户到登录页"}
1.2)输入正确密码,控制台打印“登录成功!”,浏览器跳转index.html,显示:index测试页面
2)application.properties中配置JSON,输入localhost:8080/user,控制台打印如下信息,
浏览器显示:{"content":"访问的服务需要身份认证,请引导用户到登录页"}
2.1)浏览器输入:localhost:8080/index.html,控制台打印如下信息,并跳转登录页。
2.3)输入错误密码,控制台打印“登录失败!”,浏览器显示:{"content":"坏的凭证"}
2.4)输入正确密码,控制台打印“登录成功!”,浏览器显示登录信息:
问题:为什么会打印两次:“引发跳转的请求”?
五、图形验证码
1、构建与配置
1)ImageCode.java
public classImageCode{privateBufferedImage image;privateString code;privateLocalDateTime expireTime;publicBufferedImage getImage() {returnimage;
}public voidsetImage(BufferedImage image) {this.image =image;
}public ImageCode(BufferedImage image, String code, intexpireIn) {this.image =image;this.code =code;this.expireTime =LocalDateTime.now().plusSeconds(expireIn);
}publicString getCode() {returncode;
}public voidsetCode(String code) {this.code =code;
}publicLocalDateTime getExpireTime() {returnexpireTime;
}public voidsetExpireTime(LocalDateTime expireTime) {this.expireTime =expireTime;
}public booleanisExpried() {returnLocalDateTime.now().isAfter(expireTime);
}
}
2)ValidateCodeController.java
@RestControllerpublic classValidateCodeController {
@GetMapping("/code/image")public void createCode(HttpServletRequest request,HttpServletResponse response) throwsException {
ImageCode imageCode=createImageCode(request);
request.getSession().setAttribute("imageCodeSession", imageCode);
ImageIO.write(imageCode.getImage(),"JPEG", response.getOutputStream());
}privateImageCode createImageCode(HttpServletRequest request) {int width=67;int height=23;
BufferedImage image=newBufferedImage(width, height,BufferedImage.TYPE_INT_RGB);
Graphics g=image.getGraphics();
Random random=newRandom();
g.setColor(getRandColor(200,250));
g.fillRect(0,0, width, height);
g.setFont(new Font("TIME NEW ROMAN", Font.ITALIC, 20));
g.setColor(getRandColor(160,200));for(int i=0;i<155;i++) {int x=random.nextInt(width);int y=random.nextInt(height);int xl=random.nextInt(12);int yl=random.nextInt(12);
g.drawLine(x, y, x+xl,y+yl);
}
String sRand="";for(int i=0;i<4;i++) {
String rand=String.valueOf(random.nextInt(10));
sRand+=rand;
g.setColor(new Color(20+random.nextInt(110),20+random.nextInt(110), 20+random.nextInt(110)));
g.drawString(rand,13*i+6,16);
}
g.dispose();return new ImageCode(image,sRand,60);
}private Color getRandColor(int fc, intbc) {
Random random=newRandom();if(fc>255) {
fc=255;
}if(bc>255) {
bc=255;
}int r=fc+random.nextInt(bc-fc);int g=fc+random.nextInt(bc-fc);int b=fc+random.nextInt(bc-fc);return newColor(r,g,b);
}
}
3)ValidateCodeException.java
public class ValidateCodeException extendsAuthenticationException {private static final long serialVersionUID = 1L;publicValidateCodeException(String msg) {super(msg);
}
}
4)ValidateCodeFilter.java
@Componentpublic class ValidateCodeFilter extendsOncePerRequestFilter{
@AutowiredprivateAuthenticationFailureHandler authenticationFailureHandler;
@Overrideprotected voiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throwsServletException, IOException {if(StringUtils.equals("http://localhost:8080/authentication/form",request.getRequestURL()+"") &&StringUtils.equalsIgnoreCase(request.getMethod(),"post")) {try{
validate(request);
}catch(ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request, response, e);return;
}
}
filterChain.doFilter(request, response);
}private voidvalidate(HttpServletRequest request){
ImageCode codeInSession= (ImageCode)request.getSession().getAttribute("imageCodeSession");
String codeInRequest= request.getParameter("imageCode");if(StringUtils.isBlank(codeInRequest)) {throw new ValidateCodeException("验证码的值不能为空");
}if (codeInSession == null) {throw new ValidateCodeException("验证码不存在");
}if(codeInSession.isExpried()) {
request.getSession().removeAttribute("imageCodeSession");throw new ValidateCodeException("验证码已过期");
}if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {throw new ValidateCodeException("验证码不匹配");
}
request.getSession().removeAttribute("imageCodeSession");
}
}
5)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig2 extendsWebSecurityConfigurerAdapter{
@AutowiredprivateAuthenticationSuccessHandler authenticationSuccessHandler;
@AutowiredprivateAuthenticationFailureHandler authenticationFailureHandler;
@AutowiredprivateValidateCodeFilter validateCodeFilter;
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);//认证前添加验证码过滤器
http.formLogin()
.loginPage("/authentication/require")//指定需要认证时路径
.loginProcessingUrl("/authentication/form")//指定登录页面中表单的url
.successHandler(authenticationSuccessHandler)//认证成功后自定义处理逻辑
.failureHandler(authenticationFailureHandler);//认证失败后自定义处理逻辑
http.authorizeRequests()
.antMatchers("/login.html").permitAll()//该路径不需要身份认证
.antMatchers("/authentication/require").permitAll()
.antMatchers("/code/image").permitAll()//图片验证码
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
}
6)static/login.html
登录标准登录页面
表单登录
用户名 | |
密码 | |
图片验证码 | |
登录 |
2、说明
1)验证码处理流程为:生成验证码->放在Session中->验证->清空Session
2)过滤器OncePerRequestFilter,每一次请求只进入一次该过滤器
六、记住我功能
1、构建与配置
1)pom.xml添加以下依赖
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
2)application.properties
project.security.loginType=REDIRECT
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3309/springsecurity
spring.datasource.username=root
spring.datasource.password=123456
3)login.html
登录标准登录页面
表单登录
用户名 | |
密码 | |
图片验证码 | |
记住我 | |
登录 |
4)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
@AutowiredprivateAuthenticationSuccessHandler authenticationSuccessHandler;
@AutowiredprivateAuthenticationFailureHandler authenticationFailureHandler;
@AutowiredprivateValidateCodeFilter validateCodeFilter;
@Autowiredprivate DataSource dataSource;
@Autowired
privateUserDetailsService userDetailsService;
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);//认证前添加验证码过滤器http.formLogin()
.loginPage("/authentication/require")//指定需要认证时路径
.loginProcessingUrl("/authentication/form")//指定登录页面中表单的url
.successHandler(authenticationSuccessHandler)//认证成功后自定义处理逻辑
.failureHandler(authenticationFailureHandler);//认证失败后自定义处理逻辑
http.rememberMe()//记住我
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60*60*1)//记住我1小时.userDetailsService(userDetailsService);
http.authorizeRequests()
.antMatchers("/login.html").permitAll()//该路径不需要身份认证
.antMatchers("/authentication/require").permitAll()
.antMatchers("/code/image").permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
@Beanpublic PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository=new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
tokenRepository.setCreateTableOnStartup(true);//第一次运行开启,创建数据库的表,以后不需要,注释掉
returntokenRepository;
}
}
2、测试
1)启动项目,数据库SpringSecurity中默认创建persistent_logins表,结构如下:
2)访问http://localhost:8080/index.html,用账号user登录,persistent_logins表中存了user账号的信息
3)重启项目,再次访问http://localhost:8080/index.html,无需登录直接进入index.html页面
4)验证记住我时间
4.1)设置为1分钟,清空表persistent_logins,启动项目,浏览器输入http://localhost:8080/index.html,勾选记住我,登录。停止项目。
4.2)一分钟后,启动项目,输入http://localhost:8080/index.html,发现需要登录,验证生效。勾选记住我,登录。
4.3)查询persistent_logins,发现里面有两条user信息,分别是两次登录时保存的,如下:
七、Session管理
1、设置超时时间
application.properties,新增以下配置,Session配置为1分钟(SpringBoot中最小一分钟)
server.servlet.session.timeout=1m
2、设置超时后跳转地址
1)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.sessionManagement()
.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
... ...
}
}
2)SecurityController.java
@RestControllerpublic classSecurityController {
... ...
@GetMapping("/session/invalid")
@ResponseStatus(code=HttpStatus.UNAUTHORIZED)publicSimpleResponse sessionInvalid() {
System.out.println("session失效");return new SimpleResponse("session失效");
}
}
测试:启动项目,访问localhost:8080/index.html,登陆,停止项目后再次启动,刷新该地址,浏览器出现“session失效”。
3、设置单机登陆
WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.sessionManagement()
.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
.maximumSessions(1);//最大session数量,1代表只能一个登录,后面的会把前面的踢掉
... ...
}
}
测试:Chrome浏览器访问localhost:8080/index.html,用sl登陆;换360浏览器访问该地址,再次用sl登陆,刷新Chrome,如下:
4、Session达到最大数后,阻止后面的登录
WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
http.sessionManagement()
.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
.maximumSessions(1)//最大session数量,1代表只能一个登录,后面的会把前面的踢掉
.maxSessionsPreventsLogin(true);//session数量达到了后,阻止后面的登录... ...
}}
测试:用两个浏览器先后登录,第二个登录后页面显示:{"content":"访问的服务需要身份认证,请引导用户到登录页"}
5、Session被踢掉后的处理
1)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.sessionManagement()
.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
.maximumSessions(1)//最大session数量,1代表只能一个登录,后面的会把前面的踢掉//.maxSessionsPreventsLogin(true)//session数量达到了后,阻止后面的登录
.expiredSessionStrategy(new MyexpiredSessionStrategy());//踢掉先登录的session,先登录的再请求后端进入该类的方法... ...
}
}
2)MyexpiredSessionStrategy.java
public class MyexpiredSessionStrategy implementsSessionInformationExpiredStrategy{
@Overridepublic void onExpiredSessionDetected(SessionInformationExpiredEvent event) throwsIOException, ServletException {
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write("并发登录");
}
}
测试 :先用Chrome浏览器登录,在用360登录,然后刷新Chrome,页面出现“并发登录” 。
说明:不能与maxSessionsPreventsLogin同时设置,否则不会生效,会执行阻止后面的登录的逻辑。
八、退出操作
1、默认退出操作
1)执行退出操作做的事:使当前Session失效;清除与当前用户相关的remember-me记录;清除当前的SecurityContext;重定向到登录页。
2)添加退出的超级链接:退出,点击就能退出。
2、自定义退出连接
1)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.logout().logoutUrl("/signOut")//指定退出的连接,默认/logout
... ...
}
}
2)添加退出的超级链接:退出,点击就能退出。
3、自定义退出后跳转的url
WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.sessionManagement()//.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
.maximumSessions(1)//最大session数量,1代表只能一个登录,后面的会把前面的踢掉//.maxSessionsPreventsLogin(true)//session数量达到了后,阻止后面的登录
.expiredSessionStrategy(new MyexpiredSessionStrategy());//踢掉先登录的session,先登录的再请求后端进入该类的方法
http.logout()
.logoutUrl("/signOut")//指定退出的连接,默认/logout
.logoutSuccessUrl("/logout.html");//自动退出后跳转的url,默认跳到登录的url上http.authorizeRequests()
.antMatchers("/login.html","/authentication/require","/code/image","/session/invalid","/logout.html").permitAll()//该路径不需要身份认证
.anyRequest()
.authenticated();
http.csrf().disable();//先禁止掉跨站请求伪造防护功能
}
}
说明:必须去掉 invalidSessionUrl 配置项,否则点击退出后,会跳转到 invalidSessionUrl指定的连接。
4、自定义退出后跳转处理
1)WebSecurityConfig.java
@EnableWebSecuritypublic class WebSecurityConfig extendsWebSecurityConfigurerAdapter{
... ...
@AutowiredprivateMyLogoutSuccessHandler logoutSuccessHandler;
@Overrideprotected void configure(HttpSecurity http) throwsException {
... ...
http.sessionManagement()
.invalidSessionUrl("/session/invalid")//session超时后的跳转地址,不会进入loginPage定义的地址中了
.maximumSessions(1)//最大session数量,1代表只能一个登录,后面的会把前面的踢掉//.maxSessionsPreventsLogin(true)//session数量达到了后,阻止后面的登录
.expiredSessionStrategy(new MyexpiredSessionStrategy());//踢掉先登录的session,先登录的再请求后端进入该类的方法
http.logout()
.logoutUrl("/signOut")//指定退出的连接,默认/logout//.logoutSuccessUrl("/logout.html")//自动退出后跳转的url,默认跳到登录的url上
.logoutSuccessHandler(logoutSuccessHandler)//退出成功后,自定义的操作,不能与logoutSuccessUrl同时存在
.deleteCookies("JSESSIONID");
... ...
}
}
2)MyLogoutSuccessHandler.java
@Componentpublic class MyLogoutSuccessHandler implementsLogoutSuccessHandler {
@AutowiredprivateSecurityProperties securityProperties;private ObjectMapper objectMapper=newObjectMapper();
@Overridepublic voidonLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throwsIOException, ServletException {
System.out.println("onLogoutSuccess:退出成功!");
LoginResponseType loginType=securityProperties.getLoginType();if(LoginResponseType.JSON.equals(loginType)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功!")));
}else{
response.sendRedirect("/logout.html");
}
}
}
project.security.loginType配置为REDIRECT,退出后跳转到 logout.html页面;配置为JSON,页面显示出:{"content":"退出成功!"}。
logoutSuccessHandler与logoutSuccessUrl同时配置,logoutSuccessUrl会失效。
优先级:logoutSuccessHandler > invalidSessionUrl > logoutSuccessUrl