Spring Security登录表单配置(3)

news2024/11/20 20:29:46

1、登录表单配置

1.1、快速入门

理解了入门案例之后,接下来我们再来看一下登录表单的详细配置,首先创建一个新的Spring Boot项目,引入Web和Spring Security依赖,代码如下:

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

项目创建好之后,为了方便测试,需要在application.yml中添加如下配置,将登录用户名和密码固定下来:

spring:
  security:
    user:
      name: buretuzi
      password: 123456

接下来,我们在resources/static目录下创建一个login.html页而,这个是我们自定义的登录页面:

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
    <div id="login">
        <div class="container">
            <div id="login-row" class="row justify-content-center align-items-center">
                <div id="login-column" class="col-md-6">
                    <div id="login-box" class="col-md-12">
                        <form id="login-form" class="form" action="/dologin" method="post">
                            <h3 class="text-center text-info">登录</h3>
                            <div class="form-group">
                                <label for="username" class="text-info">用户名:</label><br>
                                <input type="text" name="uname" id="username" class="form-control">
                            </div>
                            <div class="form-group">
                                <label for="password" class="text-info">密码:</label><br>
                                <input type="text" name="passwd" id="password" class="form-control">
                            </div>
                            <div class="form-group">
                                <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

这个logmt.html中的核心内容就是一个登录表单,登录表单中有三个需要注意的地方:

  • form的action,这里给的是/doLogin,表示表单要提交到/doLogin接口上。
  • 用户名输入框的name属性值为uname,当然这个值是可以自定义的,这里采用了uname。
  • 密码输入框的name属性值为passwd, passwd也是可以自定义的。

login.html定义好之后,接下来定义两个测试接口,作为受保护的资源。当用户登录成功 后,就可以访问到受保护的资源。接口定义如下:

package com.intehel.demo.controller;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class LoginController {

    @RequestMapping("/index")
    public String index(){
        return "login";
    }
    
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
    
}

最后再提供一个Spring Security的配置类:

package com.intehel.demo.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
}

在Spring Security中,如果我们需要自定义配置,基本上都是继承自WebSecurityConfigurerAdapter来实现的,当然WebSecurityConfigurerAdapter本身的配置还是比较复杂,同时也是比较丰富的,这里先不做过多的展开,仅就结合上面的代码来解释,在下节中将会对这里的配置再做更加详细的介绍。

  • 首先configure方法中是一个链式配置,当然也可以不用链式配置,每一个属性配置完毕后再从重新开始写起;
  • authorizeRequests()方法表示开启权限配置(该方法的含义其实比较复杂,在后面还会再次介绍该方法),.anyRequest().authenticated()表示所有的请求都要认证之后才能访问;
  • 有的读者会对and()方法表示疑惑,and()方法会返回HttpSecurityBuilder对象的一个 子类(实际上就是HttpSecurity),所以and()方法相当于又回到HttpSecuiity实例,重新开启 新一轮的配置。如果觉得and()方法很难理解,也可以不用and()方法,在.anyRequest().authenticated。配置完成后直接用分号(;)结束,然后通过http.formLogin()继续配置表单登录。
  • formLogin()表示开启表单登录配置,loginPage用来配置登录页面地址; loginProcessingUrl用来配置登录接口地址;defaultSuccessUrl表示登录成功后的跳转地址; failureUrl表示登录失败后的跳转地址;usernameParameter表示登录用户名的参数名称; passwordParameter表示登录密码的参数名称;permitAll表示跟登录相关的页面和接口不做拦截, 直接通过。需要注意的是,loginProcessingUrl、usernameParameter、passwordParameter 需要和login-html中登录表单的配置一致。
  • 最后的csrf().disable()表示禁用CSRF防御功能,Spring Security自带了 CSRF防御机制,但是我们这里为了测试方便,先将CSRF防御机制关闭,在后面将会详细介绍CSRF攻击与防御问题。

配置完成后,启动Spring Boot项目,浏览器地址栏中输入http://localhost:8080/index,会自动跳转到http://localhost:8080/loginNew.html页面,如图2-5所示。输入用户名和密码进行登录(用 户名为buretuzi,密码为123456),登录成功之后,就可以访问到index页面了,如图2-6所示。
在这里插入图片描述

在这里插入图片描述

经过上面的配置,我们已经成功自定义了一个登录页面出来,用户在登录成功之后,就可以访问受保护的资源了。

1.2、配置细节

当然,前面的配置比较粗糙,这里还有一些配置的细节需要和读者分享一下。

在前面的配置中,我们用defaultSuccessUrl表示用户登录成功后的跳转地址,用failureUrl 表示用户登录失败后的跳转地址。关于登录成功和登录失败,除了这两个方法可以配置之外, 还有另外两个方法也可以配置。

1.2.1、登录成功

当用户登录成功之后,除了 defaultSuccessUrl方法可以实现登录成功后的跳转之外, successForwardUrl也可以实现登录成功后的跳转,代码如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
}

defaultSuccessUrl 和 successForwardUrl 的区别如下:

  • defaultSuccessUrl表示当用户登录成功之后,会自动重定向到登录之前的地址上, 如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到defaultSuccessUrl指定的页面中。例如,用户在未认证的情况下,访问了/hello页面,此时会自动重定向到登录页面, 当用户登录成功后,就会自动重定向到/hello页面;而用户如果一开始就访问登录页面,则登录成功后就会自动重定向到defaultSuccessUrl所指定的页面中.
  • successForwardUrl则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到successForwardUrl所指定的页面;
  • defaultSuccessUrl有一个重载方法,如果重载方法的第二个参数传入true,则 defaultSuccessUrl的效果与successForwardUrl类似,即不考虑用户之前的访问地址,只要登录成功,就重定向到defaultSuccessUrl所指定的页面。不同之处在于,defaultSuccessUrl是通过重定向实现的跳转(客户端跳转),successForwardUrl则是通过服务器端跳转实现的。

无论是 defaultSuccessUrl 还是 successForwardUrl,最终所配置的都是 AuthenticationSuccessHandler接口的实例。
Spring Security中专门提供了 AuthenticationSuccessHandler接口用来处理登录成功事项:

public interface AuthenticationSuccessHandler {

	default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
		FilterChain chain, Authentication authentication)  throws IOException, ServletException{
		onAuthenticationSuccess(request, response, authentication);
		chain.doFilter(request, response);
	}
	
	void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
		Authentication authentication) throws IOException, ServletException;
		
}

由上述代码可以看到,AuthenticationSuccessHandler接口中一共定义了两个方法,其中一 个是default方法,此方法是Spring Security 5.2开始加入进来的,在处理特定的认证请求 AuthenticationFilter中会用到;另外一个非default方法,则用来处理登录成功的具体事项,其 中request和response参数好理解,authentication参数保存了登录成功的用户信息。我们将在后面的章节中详细介绍authentication参数。

AuthenticationSuccessHandler接口共有三个实现类,如图2-7所示。
在这里插入图片描述

(1) SimpleUrlAuthenticationSuccessHandler继承自 AbstractAuthenticationTargetUrlRequestHandler,通过 AbstractAuthenticationTargetUrlRequestHandler 中的 handle 方法实现请求重定向。

(2)SavedRequestAwareAuthenticationSuccessHandler在 SimpleUrlAuthenticationSuccessHandler的基础上增加了请求缓存的功能,可以记录之前请求的地址,进而在登录成功后重定向到一开始访问的地址。

(3) ForwardAuthenticationSuccessHandler的实现则比较容易,就是一个服务端跳转。

我们来重点分析 SavedRequestAwareAuthenticationSuccessHandler和ForwardAuthenticationSuccessHandler的实现。

当通过defaultSuccessUrl来设置登录成功后重定向的地址时,实际上对应的实现类就是 SavedRequestAwareAuthenticationSuccessHandler。

public class SavedRequestAwareAuthenticationSuccessHandler extends
		SimpleUrlAuthenticationSuccessHandler {
		
	protected final Log logger = LogFactory.getLog(this.getClass());
	private RequestCache requestCache = new HttpSessionRequestCache();
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);
 
			return;
		}
		clearAuthenticationAttributes(request);
		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
	
	public void setRequestCache(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
}

这里的核心方法就是onAuthenticationSuccess:

  • 首先从requestcache中获取缓存下来的请求,如果没有获取到缓存请求,就说明用户在访问登录页面之前并没有访问其他页面,此时直接调用父类的onAuthenticationSuccess方法来处理,最终会重定向到defaultSuccessUrl指定的地址。
  • 接下来会获取一个targetUrlParameter,这个是用户显式指定的、希望登录成功后重定向的地址,例如用户发送的登录请求是http://localhost:8080/doLogin?target=/hello,这就表示当用户登录成功之后,希望自动重定向到/hello这个接口,getTargetUrlParameter就是要获取重定向地址参数的key,也就是上面的target,拿到target之后,就可以获取到重定向地址了。
  • 如果 targetUrlParameter 存在,或者用户设置了 alwaysUseDefaultTargetUrl 为 true, 这个时候缓存下来的请求就没有意义了。此时会直接调用父类的onAuthenticationSuccess方法完成重定向口 targetUrlParameter存在,则直接重定向到targetUrlParameter指定的地址;alwaysUseDefaultTargetUrl 为 true,则直接重定向到 defaultSuccessUrl 指定的地址;如果 targetUrlParameter 存在并且 alwaysUseDefaultTargetUrl 为 true,则重定向到 defaultSuccessUrl 指定的地址。
  • 如果前面的条件都不满足,那么最终会从缓存请求savedRequest中获取重定向地址, 然后进行重定向操作。

这就是SavedRequestAwareAuthenticationSuccessHandler的实现逻辑,升发者也可以配置 自己的 SavedRequestAwareAuthenticationSuccessHandler,代码如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/index")
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
    SavedRequestAwareAuthenticationSuccessHandler successHandler(){
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/index");
        handler.setTargetUrlParameter("target");
        return handler;
    }
    
}

注意在配置时指定了 targetUrlParameter为target,这样用户就可以在登录请求中,通过 target来指定跳转地址了,然后我们修改一下前面login.html中的form表单:

<form id="login-form" class="form" action="/doLogin?target=/hello" method="post">
    <h3 class="text-center text-info">登录</h3>
    <div class="form-group">
        <label for="username" class="text-info">用户名:</label><br>
        <input type="text" name="uname" id="username" class="form-control">
    </div>
    <div class="form-group">
        <label for="password" class="text-info">密码:</label><br>
        <input type="text" name="passwd" id="password" class="form-control">
    </div>
    <div class="form-group">
        <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
    </div>
</form>

在form表单中,action修改/doLogin?target=/hello,这样当用户登录成功之后,就始终跳转到/hello接口了。

当我们通过successForwardUrl来设置登录成功后重定向的地址时,实际上对应的实现类 就是 ForwardAuthenticationSuccessHandler,ForwardAuthenticationSuccessHandler 的源码特别简单,就是一个服务端转发,代码如下:

public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	private final String forwardUrl;
	
	public ForwardAuthenticationSuccessHandler(String forwardUrl) {
		Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl),
				() -> "'" + forwardUrl + "' is not a valid forward URL");
		this.forwardUrl = forwardUrl;
	}
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
		Authentication authentication) throws IOException, ServletException {
		request.getRequestDispatcher(forwardUrl).forward(request, response);
	}
	
}

由上述代码可以看到,主要功能就是调用getRequestDispatcher方法进行服务端转发。 AuthenticationSuccessHandler默认的三个实现类,无论是哪一个,都是用来处理页面跳转的,有时候页面跳转并不能满足我们的需求,特别是现在流行的前后端分离开发中,用户登录成功后,就不再需要页面跳转了,只需要给前端返回一个JSON数据即可,告诉前端登录成功还是登录失败,前端收到消息之后自行处理。像这样的需求,我们可以通过自定义 AuthenticationSuccessHandler 的实现类来完成:

package com.intehel.demo.handler;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
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<String,Object>();
        resp.put("status",200);
        resp.put("msg","登录成功");
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
    
}

在自定义的 MyAuthenticationSuccessHandler中,重写 onAuthenticationSuccess方法,在该方法中,通过HttpServletResponse对象返回一段登录成功的JSON字符串给前端即可。最后, 在 SecurityConfig中配置自定义的 MyAuthenticationSuccessHandler,代码如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/loginNew.html")
                .loginProcessingUrl("/doLogin")
                .successHandler(new MyAuthenticationSuccessHandler())
                .failureUrl("/loginNew.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
}

配置完成后,重启项目,此时,当用户成功登录之后,就不会进行页面跳转了,而是返回一段JSON字符串:
在这里插入图片描述

1.2.2、登录失败

接下来看登录失败的处理逻辑。为了方便在前端页面展示登录失败的异常信息,我们首先在项目的pom.xml文件中引入thymeleaf依赖,代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.0.7.RELEASE</version>
</dependency>

然后在resources/templates目录下新建mylogin.html,代码如下:

 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin?target=/hello" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <div th:text="${SPRING SECURITY LAST EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

mylogin.html和前面的login.html基本类似,前面的login.html是静态页面,这里的 mylogin.html是thymeleaf模板页面,mylogin.html页面在form中多了一个div,用来展示登录失败时候的异常信息,登录失败的异常信息会放在request中返回到前端,开发者可以将其直接提取岀来展示。

既然mylogm.html是动态页面,就不能像静态页面那样直接访问了,需要我们给mylogin.html页面提供一个访问控制器:

package com.intehel.demo.controller;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
public class MyLoginController {

    @RequestMapping("/mylogin.html")
    public String myLogin(){
        return "mylogin";
    }
    
}

最后再在SecurityConfig中配置登录页面,代码如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

failureUrl表示登录失败后重定向到mylogin.html页面。重定向是一种客户端跳转,重定向不方便携带请求失败的异常信息(只能放在URL中)。

如果希望能够在前端展示请求失败的异常信息,可以使用下面这种方式:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureForwardUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
}

failureForwardUrl方法从名字上就可以看出,这种跳转是一种服务器端跳转,服务器端跳转的好处是可以携带登录异常信息,如果登录失败,自动跳转回登录页面后,就可以将错误信息展示出来,如图2-8所示。
在这里插入图片描述
无论是 failureUrl 还是 failureForwardUrl,最终所配置的都是 AuthenticationFailureHandler 接口的实现。Spring Security中提供了 AuthenticationFailureHandler 接口,用来规范登录失败的 实现:

public interface AuthenticationFailureHandler {

	void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 
		AuthenticationException exception) throws IOException, ServletException;
		
}

AuthenticationFailureHandler 接口中只有一个 onAuthenticationFailure 方法,用来处理登录 失败请求,request和response参数很好理解,最后的exception则表示登录失败的异常信息。 Spring Security 中为 AuthenticationFailureHandler 一共提供了五个实现类,如图:
在这里插入图片描述

  • SimpleUrlAuthenticationFailureHandler默认的处理逻辑就是通过重定向跳转到登录页 面,当然也可以通过配置forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法的底层实现逻辑就是 SimpleUrlAuthenticationFailureHandler。
  • ExceptionMappingAuthenticationFailureHandler可以实现根据不同的异常类型,映射到不同的路径。
  • ForwardAuthenticationFailureHandler表示通过服务器端跳转来重新回到登录页面, failureForwardUrl 方法的底层实现逻辑就是 ForwardAuthenticationFailureHandler。
  • AuthenticationEntryPointFailureHandler是 Spring Security 5.2 新引进的处理类,可以 通过AuthenticationEntryPoint来处理登录异常。
  • DelegatingAuthenticationFailureHandler可以实现为不同的异常类型配置不同的登录失败处理回调。

这里举一个简单的例子。假如不使用failureForwardUrl 方法,同时又想在登录失败后通过服务器端跳转回到登录页面,那么可以自定义SimpleUrlAuthenticationFailureHandler配置,并将forwardToDestination属性设置为true,代码如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(failureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
    SimpleUrlAuthenticationFailureHandler failureHandler(){
        SimpleUrlAuthenticationFailureHandler handler = 
                new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
        handler.setUseForward(true);
        return handler;
    }
    
}

这样配置之后,如果用户再次登录失败,就会通过服务端跳转重新回到登录页面,登录页而也会展示相应的错误信息,效果和failureForwardUrl 一致。

SimpleUrlAuthenticationFailureHandler的源码也很简单,我们一起来看一下实现逻辑(源码比较长,这里列出来核心部分):

public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
	protected final Log logger = LogFactory.getLog(getClass());
	private String defaultFailureUrl;
	private boolean forwardToDestination = false;
	private boolean allowSessionCreation = true;
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
	
	public SimpleUrlAuthenticationFailureHandler() {
	}
	public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
		setDefaultFailureUrl(defaultFailureUrl);
	}
	
	public void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException {
 
		if (defaultFailureUrl == null) {
			logger.debug("No failure URL set, sending 401 Unauthorized error");
 
			response.sendError(HttpStatus.UNAUTHORIZED.value(),
				HttpStatus.UNAUTHORIZED.getReasonPhrase());
		} else {
			saveException(request, exception);
 
			if (forwardToDestination) {
				logger.debug("Forwarding to " + defaultFailureUrl);
 
				request.getRequestDispatcher(defaultFailureUrl)
						.forward(request, response);
			} else {
				logger.debug("Redirecting to " + defaultFailureUrl);
				redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
			}
		}
	}
	
	protected final void saveException(HttpServletRequest request,
			AuthenticationException exception) {
		if (forwardToDestination) {
			request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
		} else {
			HttpSession session = request.getSession(false);
 
			if (session != null || allowSessionCreation) {
				request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
						exception);
			}
		}
	}
	
	public void setDefaultFailureUrl(String defaultFailureUrl) {
		Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
				() -> "'" + defaultFailureUrl + "' is not a valid redirect URL");
		this.defaultFailureUrl = defaultFailureUrl;
	}
	
	protected boolean isUseForward() {
		return forwardToDestination;
	}
	
	public void setUseForward(boolean forwardToDestination) {
		this.forwardToDestination = forwardToDestination;
	}
	
}

从这段源码中可以看到,当用户构造SimpleUrlAuthenticationFailureHandler对象的时候, 就传入了 defaultFailureUrl也就是登录失败时要跳转的地址。在onAuthenticationFailure方法中,如果发现defaultFailureUrl为null,则直接通过response返回异常信息,否则调用 saveException 方法。在 saveException 方法中,如果 fowardToDestination 属性设置为ture,表示通过服务器端跳转回到登录页面,此时就把异常信息放到request中。再回到 onAuthenticationFailure方法中,如果用户设置fowardToDestination 为 true,就通过服务器 端跳转回到登录页面,否则通过重定向回到登录页面。

如果是前后端分离开发,登录失败时就不需要页面跳转了,只需要返回JSON字符串给前端即可,此时可以通过自定义AuthenticationFailureHandler的实现类来完成,代码如下:

package com.intehel.demo.handler;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
 
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String,Object> resp = new HashMap<String,Object>();
        resp.put("status",500);
        resp.put("msg","登录失败"+exception.getMessage());
        ObjectMapper om = new ObjectMapper();
        String s = om.writeValueAsString(resp);
        response.getWriter().write(s);
    }
    
}

然后在SecurityConfig中进行配置即可:

package com.intehel.demo.config;
 
import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
    
}

配置完成后,当用户再次登录失败,就不会进行页而跳转了,而是直接返回JSON字符串, 如图2-10所示。
在这里插入图片描述

1.2.3、注销登录

Spring Security中提供了默认的注销页面,当然开发者也可以根据自己的需求对注销登录进行定制。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
    }
    
}
  • 通过.logout()方法开启注销登录配置。
  • logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout。
  • invalidateHttpSession 表示是否使 session 失效,默认为 true。
  • clearAuthentication表示是否清除认证信息,默认为true。
  • logoutSuccessUrl表示注销登录后的跳转地址。

配置完成后,再次启动项目,登录成功后,在浏览器中输入http://localhost:8080/logout就可以发起注销登录请求了,注销成功后,会自动跳转到mylogin.html页面。

如果项目有需要,开发者也可以配置多个注销登录的请求,同时还可以指定请求的方法。

package com.intehel.demo.config;
 
import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
    }
    
}

上面这个配置表示注销请求路径有两个:

  • 第一个是/logout1,请求方法是GET。
  • 第二个是/logout2,请求方法是POST。

使用任意一个请求都可以完成登录注销。
如果项目是前后端分离的架构,注销成功后就不需要页面跳转了,只需将注销成功的信息返回给前端即可,此时我们可以自定义返回内容:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessHandler((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","注销成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                })
                .and()
                .csrf().disable();
    }
    
}

配置 logoutSuccessHandler 和 logoutSuccessUrl 类似于前面所介绍的 successHandler 和defaultSuccessUrl之间的关系,只是类不同而已,因此这里不再赘述,读者可以按照我们前面的分析思路自行分析。

配置完成后,重启项目,登录成功后再去注销登录,无论是使用/logout1还是/logout2进行注销,只要注销成功后,就会返回一段JSON字符串。

如果开发者希望为不同的注销地址返回不同的结果,也是可以的,配置如下:

 @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout1注销成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout2注销成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .and()
                .csrf().disable();
    }
    
}

通过defaultLogoutSuccessHandlerFor方法可以注册多个不同的注销成功回调函数,该方法第一个参数是注销成功回调,第二个参数则是具体的注销请求。当用户注销成功后,使用了哪个注销请求,就给出对应的响应信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1109079.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

竹云荣膺2023粤港澳大湾区高质量发展标杆企业

10月12日&#xff0c;由深圳市前海深港现代服务业合作区管理局指导&#xff0c;广东省粤港澳大湾区产业协同发展联合会&#xff08;简称&#xff1a;湾区产联&#xff09;、香港大湾区工商业联合会主办的“深港合作前海向前”2023粤港澳大湾区高质量发展企业发布会、香港大湾区…

[计算机提升] 环境变量

1.5 环境变量 在Windows系统中&#xff0c;环境变量是一组参数或值&#xff0c;用于配置和控制操作系统的行为。它们用来确定系统的默认设置、执行文件路径以及其他系统行为。环境变量可以被所有的应用程序和命令行工具访问和使用&#xff0c;而不需要每次手动指定路径或设置参…

SELECT COUNT(*) 会造成全表扫描吗?

前言 SELECT COUNT(*)会不会导致全表扫描引起慢查询呢&#xff1f; SELECT COUNT(*) FROM SomeTable 网上有一种说法&#xff0c;针对无 where_clause 的 COUNT(*)&#xff0c;MySQL 是有优化的&#xff0c;优化器会选择成本最小的辅助索引查询计数&#xff0c;其实反而性能…

笔记本触摸板没反应?实用技巧助你成功修复!

笔记本电脑是我们日常工作和学习的得力工具之一&#xff0c;而触摸板作为一个重要的输入设备&#xff0c;对于操作的流畅性至关重要。然而&#xff0c;有时候我们可能会遇到笔记本触摸板没反应的问题&#xff0c;这可能会导致困扰和不便。本文将介绍解决笔记本触摸板无响应问题…

SPSS|偏度和峰度|正态性分布检验|实战小练-SPSS学习(1)

目录 学习目的软件版本基础数据实战数据准备数据初探输出结果分析两个重要统计量&#xff1a;偏度和峰度正态性检验结果其他图件输出 学习目的 检验数据集是否服从正态分布。 软件版本 IBM SPSS Statistics 26。 基础数据 一组数据&#xff0c;如&#xff1a;73 76 78 77 …

ES6 Promise

1.Promise 是异步编程的一种解决方案 const promise new Promise(function(resolve, reject) {if (/* 异步操作成功 */){resolve(value);} else {reject(error);} }) 2.Promise的三种状态 待定&#xff08;pending&#xff09;: 初始状态&#xff0c;既没有被兑现&#xff…

情绪即需求

情绪即需求 心理学认为&#xff0c;每个情绪背后都藏着一个未被满足的心里需求. 模型介绍 每一个情绪背后&#xff0c;都有一个未被满足的心理需求。情绪没有好坏之分&#xff0c;存在即合理。情绪是人类不断进化的产物&#xff0c;每一种情绪都是在保护我们&#xff0c;都有其…

一起学数据结构(9)——二叉树的链式存储及相关功能实现

目录 1. 二叉树的链式存储&#xff1a; 2. 二叉树的前序遍历&#xff1a; 3. 二叉树的中序遍历&#xff1a; 4. 二叉树的后序遍历&#xff1a; 5. 统计二叉树的结点总数 6.统计二叉树的叶子结点数&#xff1a; 7. 统计二叉树第层的结点数量&#xff1a; 8. 二叉树的销毁…

如何修改模型颜色

1、模型材质颜色介绍 在3D模型中&#xff0c;材质&#xff08;Material&#xff09;是指表面质感的特性&#xff0c;包括颜色、光泽、透明度等属性。其中&#xff0c;颜色是最基本的属性之一&#xff0c;它决定了物体表面的外观和感觉。 在现代计算机图形学中&#xff0c;通常…

使用 Typhoeus 和 Ruby 编写的爬虫程序

以下是一个使用 Typhoeus 和 Ruby 编写的爬虫程序&#xff0c;用于爬取 &#xff0c;同时使用了 jshk.com.cn/get_proxy 这段代码获取代理&#xff1a; #!/usr/bin/env rubyrequire typhoeus require jsondef get_proxyurl "https://www.duoip.cn/get_proxy"respon…

CTF是黑客大赛?新手如何入门CTF?

CTF是啥 CTF 是 Capture The Flag 的简称&#xff0c;中文咱们叫夺旗赛&#xff0c;其本意是西方的一种传统运动。在比赛上两军会互相争夺旗帜&#xff0c;当有一方的旗帜已被敌军夺取&#xff0c;就代表了那一方的战败。在信息安全领域的 CTF 是说&#xff0c;通过各种攻击手…

随手记录第十话 -- 升级SpringBoot3.0 + JDK17的踩坑记录

随着有些jar包的升级&#xff0c;JDK1.8已经不是最稳定的版本了。 前段时间接触到Web3相关&#xff0c;jar包的编译最低要JDK13了&#xff0c;碰巧另一个使用Kotlin写的jar包依赖需要17了&#xff0c;那就直接上17吧&#xff0c;同时Springboot也上到3.0。 1. 框架说明 Spri…

哪个牌子的护眼灯防蓝光效果好?2023防蓝光护眼灯推荐

可以肯定的是&#xff0c;护眼灯一般可以达到护眼的效果。 看书和写字时&#xff0c;光线应适度&#xff0c;不宜过强或过暗&#xff0c;护眼灯光线较柔和&#xff0c;通常并不刺眼&#xff0c;眼球容易适应&#xff0c;可以防止光线过强或过暗导致的用眼疲劳。如果平时生活中需…

Python国庆祝福

系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.csdn.net/article/details/1295031234漂浮爱心https://want…

软件开发线上维护方案

编写软件维护方案是确保软件系统长期稳定运行和满足不断变化需求的关键步骤。以下是编写软件维护方案的一般步骤和建议&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 文档概述&#xff1a; 开始文档…

Java (day 3)方法、数组、面向对象和异常

Java方法、数组、面向对象和异常 1.方法1.1 什么是方法&#xff1f;1.2 方法的定义1.3 方法的调用1.4 值传递和引用传递1.5 方法的重载1.6 命令行传参1.7 可变参数1.8 递归 2.数组2.1 数组概述2.2 数组声明创建2.3 三种初始化及内存分析和总结&#xff08;1&#xff09;java内存…

前端本地开发中,代理配置是如何解决跨域的?

文章目录 跨域&#xff08;Cross-Origin&#xff09;开发代理原理先说一下三个概念那代理到底是如何解决跨域的&#xff1f; 补充参考视频 跨域&#xff08;Cross-Origin&#xff09; 这里再说一下跨域的概念吧。 在Web开发中&#xff0c;浏览器限制了从一个不同来源&#xff…

【C++】415.字符串相加

题目描述&#xff1a; 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和并同样以字符串形式返回。 你不能使用任何內建的用于处理大整数的库&#xff08;比如 BigInteger&#xff09;&#xff0c;也不能直接将输入的字符串转换为整数形式。 示例1&#x…

XPS虽没流行,但还在使用!在Windows 10中打开XPS文件的最佳方法

当Windows Vista发布时&#xff0c;微软推出了XPS格式&#xff0c;这是PDF的替代品。XPS文件格式并不是什么新鲜事&#xff0c;但从未获得过多大的吸引力。 因此&#xff0c;XPS&#xff08;XML Paper Specification&#xff09;文件是微软对Adobe PDF文件的竞争对手。尽管XPS…

Kafka三种认证模式,Kafka 安全认证及权限控制详细配置与搭建

Kafka三种认证模式,Kafka 安全认证及权限控制详细配置与搭建。 Kafka三种认证模式 使用kerberos认证 bootstrap.servers=hadoop01.com:9092,hadoop02.com:9092,hadoop03.com:9092,hadoop04.com:9092 security.