java spring 登录_配置表单登录_Spring Security 4官方文档中文翻译与源码解读教程_田守枝Java技术博客...

你可能想知道登录表单页面来自于什么地方,你什么时候会被提示需要登录。由于在之前的配置中我们并没有显示的指定登录表单的页面URL,Spring Security就会自动生成一个登录页面,并且对用户的登录操作进行处理。

在开始学习的时候,使用SpringSecurity自动生成的登录页面,是一个很好的开始。但是,大多数情况下,我们可能会希望使用自己的登录页面。

本节通过一个完整的案例说明,如何在Spring Security中自定义登录页面。效果如下动态图所示:

f10cef659479817c5613d2156a30d5f4.gif

依然老套路,先实现功能,再进行源码分析。

1 自定义表单登录实现

整个项目的目录结构如下所示:

9dbd2bb07fe771ad8b0ea1992ec1844c.png

web.xmlweb-app PUBLIC

"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

"http://java.sun.com/dtd/web-app_2_3.dtd">

Archetype Created Web Application

springmvc

org.springframework.web.servlet.DispatcherServlet

contextConfigLocation

/WEB-INF/spring-servlet.xml

2

springmvc

/

/index.html

spring-servlet.xml<?xml  version="1.0" encoding="UTF-8"?>

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:mvc="http://www.springframework.org/schema/mvc"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd

http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

index.html

这里首页我们不进行权限保护,首页里面通过一个超链接访问一个受保护的页面,此时会跳转到登录页面

index page

access privilege.html

privilege.html

privileged page

login.htmlhtml>

spring secuirty 自定义登录页面

自定义登录表单

用户名

placeholder="请输入用户名">

密码

placeholder="请输入密码">

登录

success.html

login success page

error.html

login error page

SecurityWebApplicationInitializer.javapackage com.tianshouzhi.security;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityWebApplicationInitializer

extends AbstractSecurityWebApplicationInitializer {

}

SecurityConfig.javapackage com.tianshouzhi.security;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth

.inMemoryAuthentication()

.withUser("user").password("password").roles("USER");

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.antMatchers("/index.html").permitAll()//访问index.html不要权限验证

.anyRequest().authenticated()//其他所有路径都需要权限校验

.and()

.csrf().disable()//默认开启,这里先显式关闭

.formLogin()  //内部注册 UsernamePasswordAuthenticationFilter

.loginPage("/login.html") //表单登录页面地址

.loginProcessingUrl("/login")//form表单POST请求url提交地址,默认为/login

.passwordParameter("password")//form表单用户名参数名

.usernameParameter("username") //form表单密码参数名

.successForwardUrl("/success.html")  //登录成功跳转地址

.failureForwardUrl("/error.html") //登录失败跳转地址

//.defaultSuccessUrl()//如果用户没有访问受保护的页面,默认跳转到页面

//.failureUrl()

//.failureHandler(AuthenticationFailureHandler)

//.successHandler(AuthenticationSuccessHandler)

//.failureUrl("/login?error")

.permitAll();//允许所有用户都有权限访问登录页面

}

}

运行程序

下面通过一个gif动态图演示了整个效果

f10cef659479817c5613d2156a30d5f4.gif

首先,访问首页http://localhost:8080,因为我们在SecurityConfig类中,配置了首页不需要权限控制,所以可以直接访问index.html。

其次,在首页中,有一个访问受保护的页面的超链接,当我们点击时,就会跳转到登录页面login.html

接着,我们输入错误的用户名密码,因此会跳error.html

最后,我们输入正确的用户名密码,因此会跳success.html

2 源码解读

在SecurityConfig类的configure方法中,我们首先设置了首页index.html不需要权限认证,而其他url都需要权限认证.authorizeRequests()

.antMatchers("/index.html").permitAll()//访问index.html不要权限验证

.anyRequest().authenticated()//其他url都需要验证

接着我们禁用了csrf,因为在我们的登录表单暂时没有使用到,在后面我们将会介绍在spring security中如何使用csrf。.csrf().disable()

接下来就是表单登录的核心配置了.formLogin()  //内部注册 UsernamePasswordAuthenticationFilter

.loginPage("/login.html") //表单登录页面地址

.passwordParameter("password")//form表单用户名参数名

.usernameParameter("username") //form表单密码参数名

.loginProcessingUrl("/login")//form表单POST请求url提交地址,默认为/login

.successForwardUrl("/success.html")  //登录成功跳转地址

.failureForwardUrl("/error.html") //登录失败跳转地址

//.defaultSuccessUrl()//如果用户没有访问受保护的页面,默认跳转到页面

//.failureUrl()

//.failureHandler(AuthenticationFailureHandler)

//.successHandler(AuthenticationSuccessHandler)

//.failureUrl("/login?error")

.permitAll();//允许所有用户都有权限访问登录页面

2.1、formLogin方法

我们首先调用了HttpSecurity对象的formLogin()方法,其作用是在spring security中注册一个UsernamePasswordAuthenticationFilter,用于对用户表单中提交的用户名/密码参数进行校验。

org.springframework.security.config.annotation.web.builders.HttpSecurity#formLoginpublic FormLoginConfigurer formLogin() throws Exception {

return getOrApply(new FormLoginConfigurer());

}

在HttpSecurity的formLogin方法中,通过FormLoginConfigurer注册一个UsernamePasswordAuthenticationFilter。FormLoginConfigurer的构造方法如下所示:public FormLoginConfigurer() {

super(new UsernamePasswordAuthenticationFilter(), null);

usernameParameter("username");

passwordParameter("password");

}

这里除了注册UsernamePasswordAuthenticationFilter,还通过usernameParameter()和passwordParameter()方法,指定了登录form表单中,用户名和密码的参数名默认是username和password。

UsernamePasswordAuthenticationFilter的部分源码如下所示:public class UsernamePasswordAuthenticationFilter extends

AbstractAuthenticationProcessingFilter {

...

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {

super(new AntPathRequestMatcher("/login", "POST"));//指定form表单的action属性值为/login,且提交方式必须为post

}

//登录表单提交时,此方法会被回调,对用户名和密码进行校验

public Authentication attemptAuthentication(HttpServletRequest request,

HttpServletResponse response) throws AuthenticationException {

if (postOnly && !request.getMethod().equals("POST")) {

throw new AuthenticationServiceException(

"Authentication method not supported: " + request.getMethod());

}

String username = obtainUsername(request); //内部调用request.getParameter(usernameParameter)获得用户名

String password = obtainPassword(request);//内部调用request.getParameter(passwordParameter)获得密码

if (username == null) {

username = "";

}

if (password == null) {

password = "";

}

username = username.trim();

//将用户名和密码封装到一个UsernamePasswordAuthenticationToken对象中

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(

username, password);

// Allow subclasses to set the "details" property

setDetails(request, authRequest);

//对封装到UsernamePasswordAuthenticationToken中的用户名和密码进行校验

return this.getAuthenticationManager().authenticate(authRequest);

}

....

protected String obtainPassword(HttpServletRequest request) {

return request.getParameter(passwordParameter);

}

protected String obtainUsername(HttpServletRequest request) {

return request.getParameter(usernameParameter);

}

....

}

在UsernamePasswordAuthenticationFilter中,指定了form表单的method属性必须为post,同时指定了form的action属性值默认/login。当用户表单提交时,attemptAuthentication方法会被回调,这个方法内部会通过HttpServletRequest.getParameter的方式,获得表单中填写的用户名和密码的值,封装成一个UsernamePasswordAuthenticationToken对象,然后进行校验。

attemptAuthentication方法是在UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter的doFilter方法中回调的,并根据验证的结果进行相应的处理。

AbstractAuthenticationProcessingFilter#doFilter源码如下所示public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

HttpServletResponse response = (HttpServletResponse) res;

//判断是否需要进行验证,其实就是判断请求的路径是否是/login,如果不是/login,说明不是form表单登录请求,则不需要进行验证

if (!requiresAuthentication(request, response)) {

chain.doFilter(request, response);

return;//如果不需要验证,则直接return,下面的代码逻辑都不会走了

}

if (logger.isDebugEnabled()) {

logger.debug("Request is to process authentication");

}

//如果需要验证

Authentication authResult;

try {

//调用attemptAuthentication方法进行验证,并获得验证结果Authentication对象。

authResult = attemptAuthentication(request, response);

if (authResult == null) {

// return immediately as subclass has indicated that it hasn't completed

// authentication

return;

}

//这段代码主要是为了防止session劫持(session fixation attacks),我们将在后面介绍,什么是session劫持

sessionStrategy.onAuthentication(authResult, request, response);

}

catch (InternalAuthenticationServiceException failed) {

logger.error(

"An internal error occurred while trying to authenticate the user.",

failed);

// 验证失败

unsuccessfulAuthentication(request, response, failed);

return;

}

catch (AuthenticationException failed) {

// 验证失败

unsuccessfulAuthentication(request, response, failed);

return;

}

// Authentication success

if (continueChainBeforeSuccessfulAuthentication) {

chain.doFilter(request, response);

}

//验证成功

successfulAuthentication(request, response, chain, authResult);

}

上述代码中,最重要的就是attemptAuthentication方法,这是一个抽象方法,UsernamePasswordAuthenticationFilter中进行了实现,以实现自己的验证逻辑,也就是前面我看到的从HttpServletRequest对象中获得用户名和密码,进行验证。

AbstractAuthenticationProcessingFilter#attemptAuthentication方法声明如下所示:public abstract Authentication attemptAuthentication(HttpServletRequest request,

HttpServletResponse response) throws AuthenticationException, IOException,

ServletException;

当验证失败时,需要抛出AuthenticationException异常。具体来说,验证失败可能分为2种情况:验证服务失败,例如我们从数据库中查询用户名和密码验证用户,但是数据库服务挂了,此时抛出InternalAuthenticationServiceException异常

验证参数失败,例如用户输入的用户名和密码错误,此时抛出AuthenticationException异常

InternalAuthenticationServiceException是AuthenticationException的子类,因此我们看到在上面的catch代码块中,先捕获前者,再捕获后者。

无论是哪一种情况,都会调用unsuccessfulAuthentication方法,此方法内部会跳转到我们定义的登录失败页面。

如果验证成功,会调用successfulAuthentication方法,默认情况下,这个方法内部会将用户登录信息放到Session中,然后跳转到我们定义的登录成功页面。

关于unsuccessfulAuthentication和successfulAuthentication源码分析,见下文。

2.2 loginPage方法

FormLoginConfigurer的loginPage方法用于自定义登录页面。如果我们没有调用这个方法,spring security将会注册一个DefaultLoginPageGeneratingFilter,这个filter的generateLoginPageHtml方法会帮我们生成一个默认的登录页面,也就是我们前面章节看到的那样。在本节案例中,我们自定义了登录页面地址为/login.html,则DefaultLoginPageGeneratingFilter不会被注册。

FormLoginConfigurer继承了AbstractAuthenticationFilterConfigurer,事实上,loginPage方法定义在这个类中。AbstractAuthenticationFilterConfigurer中维护了一个customLoginPage字段,用于记录用户是否设置了自定义登录页面。

AbstractAuthenticationFilterConfigurer#loginPageprotected T loginPage(String loginPage) {

setLoginPage(loginPage);//当spring security判断用户需要登录时,会跳转到loginPage指定的页面中

updateAuthenticationDefaults();

this.customLoginPage = true; //标记用户使用了自定义登录页面

return getSelf();

}

而在FormLoginConfigurer初始化时,会根据customLoginPage的值判断是否注册DefaultLoginPageGeneratingFilter。参见:

FormLoginConfigurer#initDefaultLoginFilterprivate void initDefaultLoginFilter(H http) {

DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http

.getSharedObject(DefaultLoginPageGeneratingFilter.class);

//如果没有自定义登录页面,则使用DefaultLoginPageGeneratingFilter

if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {

loginPageGeneratingFilter.setFormLoginEnabled(true);

loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());

loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());

loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());

loginPageGeneratingFilter.setFailureUrl(getFailureUrl());

loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());

}

}

在前面的配置中,我们指定的loginPage是一个静态页面login.html,我们也可以定义一个Controller,返回一个ModelAndView对象跳转到登录页面,注意Controller中方法的@RequestMapping注解的值,需要和loginPage方法中的值相对应。

回顾前面分析的AbstractAuthenticationFilterConfigurer#loginPage方法源码,我们提到,当spring security判断一个用户需要登录时会跳转到指定的登录页面上。在loginPage方法中,首先调用了setLoginPage(loginPage);方法,源码如下所示:

AbstractAuthenticationFilterConfigurer#setLoginPageprivate void setLoginPage(String loginPage) {

this.loginPage = loginPage;

this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);

}

可以看到这里,还创建了一个LoginUrlAuthenticationEntryPoint对象,事实上,跳转的逻辑就是在这个类中完成的。

LoginUrlAuthenticationEntryPoint#commence//commence方法用户判断跳转到登录页面时,是使用重定向(redirect)的方式,还是使用转发(forward)的方式

public void commence(HttpServletRequest request, HttpServletResponse response,

AuthenticationException authException) throws IOException, ServletException {

String redirectUrl = null;

if (useForward) {//使用转发的方法,用户浏览器地址url不会发生改变

if (forceHttps && "http".equals(request.getScheme())) {

// First redirect the current request to HTTPS.

// When that request is received, the forward to the login page will be

// used.

redirectUrl = buildHttpsRedirectUrlForRequest(request);

}

if (redirectUrl == null) {

String loginForm = determineUrlToUseForThisRequest(request, response,

authException);

if (logger.isDebugEnabled()) {

logger.debug("Server side forward to: " + loginForm);

}

RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

dispatcher.forward(request, response);

return;

}

}

else {//使用重定向的方式,用户浏览器地址url发生改变

// redirect to login page. Use https if forceHttps true

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

}

redirectStrategy.sendRedirect(request, response, redirectUrl);

}

而commence方法的调用是在ExceptionTranslationFilter#handleSpringSecurityException中进行的。spring security中的验证错误会统一放到ExceptionTranslationFilter处理,其handleSpringSecurityException中,会判断异常的类型,如果是AuthenticationException类型的异常,则会调用sendStartAuthentication方法,进行跳转。如下protected void sendStartAuthentication(HttpServletRequest request,

HttpServletResponse response, FilterChain chain,

AuthenticationException reason) throws ServletException, IOException {

// SEC-112: Clear the SecurityContextHolder's Authentication, as the

// existing Authentication is no longer considered valid

SecurityContextHolder.getContext().setAuthentication(null);

requestCache.saveRequest(request, response);

logger.debug("Calling Authentication entry point.");

authenticationEntryPoint.commence(request, response, reason);//进行页面跳转

}

2.3 usernameParameter、passwordParameter方法

通过前面的分析,我们知道在UsernamePasswordAuthenticationFilter中,是通过HttpServletRequest的getParamter方法来获得用户性和密码参数值的。默认的参数名是"username"、"password"。要求登录页面的form表单中的参数名,与此必须匹配。

我们可以通过调用FormLoginConfigurer的相关方法,重新定义参数名,例如formLogin()

.usernameParameter("user") //form表单密码参数名

.passwordParameter("pwd")//form表单用户名参数名

此时login.html页面的form表单也要进行相应的修改,如:

2.4 loginProcessingUrl方法

在UsernamePasswordAuthenticationFilter的构造方法中,我们可以看到默认的拦截路径是"/login",也就是说,只有form表单的action属性值也为"/login"时,UsernamePasswordAuthenticationFilter的attemptAuthentication方法才会执行,因为在父类AbstractAuthenticationProcessingFilter中,会先对请求url进行判断,只有匹配上"/login"时,才会回调attemptAuthentication方法。

如果用户想修改表单提交地址的话,可以通过FormLoginConfigurer的loginProcessingUrl方法,如下.formLogin()

.loginProcessingUrl("/admin/login")//form表单POST请求url提交地址,需要与form表单action属性值对应,默认为/login

此时login.html中form元素action属性值需要进行相应的修改,如

...

2.5 .successForwardUrl、failureForwardUrl、defaultSuccessUrl方法

FormLoginConfigurer的successForwardUrl、failureForwardUrl方法分别用于定义登录成功和失败的跳转地址。.formLogin()

.successForwardUrl("/success.html")

.failureForwardUrl("/error.html")

这两个方法内部实现如下:public FormLoginConfigurer successForwardUrl(String forwardUrl) {

successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));

return this;

}

public FormLoginConfigurer failureForwardUrl(String forwardUrl) {

failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));

return this;

}

可以看到,内部利用跳转路径url分别构建了ForwardAuthenticationSuccessHandler、ForwardAuthenticationFailureHandler,用于跳转。

其中successHandler和failureHandler方法都继承自父类AbstractAuthenticationFilterConfigurer,用于给父类中维护的successHandler、failureHandler字段赋值。

因此FormLoginConfigurer的successForwardUrl、failureForwardUrl方法实际上只是AbstractAuthenticationFilterConfigurer的successHandler、failureHandler方法的一种快捷方式而已,我们可以直接调用successHandler、failureHandler方法,来定义跳转方式。

ForwardAuthenticationSuccessHandler和ForwardAuthenticationFailureHandler的实现类似,以前者为例,其源码如下: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;

}

//登录成功时,通过调用此方法进行页面的跳转,forward方式

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)

throws IOException, ServletException {

request.getRequestDispatcher(forwardUrl).forward(request, response);

}

}

可以看到这个方法里面就是利用request.getRequestDispatcher来进行转发。

回顾2.1 我们分析formLogin方法源码时,在AbstractAuthenticationProcessingFilter的doFilter方法中,验证成功或者失败分别会回调successfulAuthentication、unsuccessfulAuthentication方法。事实上ForwardAuthenticationSuccessHandler的onAuthenticationSuccess方法就是在AbstractAuthenticationProcessingFilter的successfulAuthentication中被回调的。

AbstractAuthenticationProcessingFilter#successfulAuthentication//用户验证成功后,回调此方法

protected void successfulAuthentication(HttpServletRequest request,

HttpServletResponse response, FilterChain chain, Authentication authResult)

throws IOException, ServletException {

if (logger.isDebugEnabled()) {

logger.debug("Authentication success. Updating SecurityContextHolder to contain: "

+ authResult);

}

//1、记录用户登录成功信息,默认放入到Session中

SecurityContextHolder.getContext().setAuthentication(authResult);

//2、如果开启了自动登录(用于支持我们经常在各个网站登录页面上的"记住我"复选框)

rememberMeServices.loginSuccess(request, response, authResult);

//3、 发布用户登录成功事件,我们可以定义一个bean,实现spring的ApplicationListener接口,则可以获取到所有的用户登录事件

if (this.eventPublisher != null) {

eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(

authResult, this.getClass()));

}

//4、最后调用ForwardAuthenticationSuccessHandler的onAuthenticationSuccess方法,进行页面的转发

successHandler.onAuthenticationSuccess(request, response, authResult);

}

可以看到验证成功后,处理分为四步,目前我们只关注最后一步的转发操作。

由于ForwardAuthenticationSuccessHandler、ForwardAuthenticationFailureHandler都是进行转发,因此不论用户登录成功还是失败,浏览器地址栏的地址都是post请求地址"/login"的路径。

75d593edf6b90d9ab5e6b032afac4b55.png

f6d475094749f445a1163fe80bbe168a.png

事实上,我们希望的结果是:如果用户没有访问受保护的资源,例如通过地址栏直接访问登录页面,那么登录后,就默认跳转到首页。

用户在访问一个受保护的页面时(称之为目标页面:target page),如果需要验证,就跳转到登录页面。如果登录成功,那么就久直接跳转到目标页面,如果登录失败,还继续停留在登录页面。

事实上,这也是spring security的默认行为,及如果我们不进行任何跳转页面的配置,默认采用的就是上述逻辑。

此时我们可以这样修改配置如下:.formLogin()

.loginPage("/login.html")

.passwordParameter("password")

.usernameParameter("username")

.loginProcessingUrl("/login")

.permitAll();

下面通过2个gif动态图来演示修改后配置效果

1、直接访问http://localhost:8080/login.html,登录成功后,默认跳转到首页

d78fd4603c680b6fe193c7e940e80b86.gif

可以看到,直接访问登录页面后,如果登录成功,默认就跳转到首页了。我们可以通过配置defaultSuccessUrl来修改默认跳转到的页面.formLogin()

.defaultSuccessUrl("/index.html")//如果没有访问受保护的资源,登录成功后,默认跳转到的页面,默认值为"/"

2、访问首页http://localhost:8080/,点击受保护的页面,登录成功跳转到目标页面,失败依然停留在登录页面

16799ee79d70ae6fe07ebf3f6cad0951.gif

这里需要注意的是,在登录失败后,浏览器的地址会变为http://localhost:8080/login?error,也就是说,在loginProccessUrl方法指定的url后面加上?error。

由于这里使用的静态页面,所以无法展示错误信息。事实上,spring security会将错误信息放到Session中,key为SPRING_SECURITY_LAST_EXCEPTION,如果你使用jsp或者其他方式,则可以从session中把错误信息获取出来,常见的错误信息包括:用户名不存在:UsernameNotFoundException;

密码错误:BadCredentialException;

帐户被锁:LockedException;

帐户未启动:DisabledException;

密码过期:CredentialExpiredException;等等!

现在我们来分析,spring security是如何做到这种登录成功/失败跳转的挑战逻辑的:

登录成功

在AbstractAuthenticationFilterConfigurer中,默认的successHandler是SavedRequestAwareAuthenticationSuccessHandler。当访问受保护的目标页面,登录后直接跳转到目标页面,以及直接访问登录页面,登录后默认跳转到首页,就是通过SavedRequestAwareAuthenticationSuccessHandler这个类完成的。public abstract class AbstractAuthenticationFilterConfigurer...{

....

private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

}

由于前面的案例中,我们调用了successForwardUrl方法,因此successHandler的默认值被覆盖为ForwardAuthenticationSuccessHandler,因此失去了这个功能。

具体来说,SavedRequestAwareAuthenticationSuccessHandler有一个RequestCache对象,当用户访问受保护的页面时,spring security会将当前请求HttpServletRequest对象信息放到这个RequestCache中。

参见ExceptionTranslationFilter#sendStartAuthentication方法//需要进行登录验证

protected void sendStartAuthentication(HttpServletRequest request,

HttpServletResponse response, FilterChain chain,

AuthenticationException reason) throws ServletException, IOException {

SecurityContextHolder.getContext().setAuthentication(null);

//缓存当前request对象,其中包含了用户想访问的目标页面登录信息

requestCache.saveRequest(request, response);

logger.debug("Calling Authentication entry point.");

//跳转到登录页面,这个之前已经分析过,不再赘述

authenticationEntryPoint.commence(request, response, reason);

}

当用户登录成功后,AbstractAuthenticationProcessingFilter#successfulAuthentication方法会被回调,这个方法源码之前也已经分析过,最后一步是调用successHandler.onAuthenticationSuccess(request, response, authResult);

此时就是调用SavedRequestAwareAuthenticationSuccessHandler#onAuthenticationSuccesspublic class SavedRequestAwareAuthenticationSuccessHandler extends

SimpleUrlAuthenticationSuccessHandler {

protected final Log logger = LogFactory.getLog(this.getClass());

private RequestCache requestCache = new HttpSessionRequestCache();//这是一个session级别的缓存

@Override

public void onAuthenticationSuccess(HttpServletRequest request,

HttpServletResponse response, Authentication authentication)

throws ServletException, IOException {

//1、从requestCache中获得之前保存的HttpServletRequest对象,SavedRequest是对HttpServletRequest的封装

SavedRequest savedRequest = requestCache.getRequest(request, response);

//2、如果用户直接访问的登录页面,则savedRequest为空,跳转到默认页面

if (savedRequest == null) {

super.onAuthenticationSuccess(request, response, authentication);

return;

}

/*3 如果设置为targetUrlParameter参数,会从当前请求request对象,查看请求url是否包含要跳转到的路径参数,如果有则跳转到这个url,

这个逻辑是在父类SimpleUrlAuthenticationSuccessHandler中进行的*/

String targetUrlParameter = getTargetUrlParameter();

if (isAlwaysUseDefaultTargetUrl()

|| (targetUrlParameter != null && StringUtils.hasText(request

.getParameter(targetUrlParameter)))) {

//从requestCache中移除之前保存的request,以免缓存过多,内存溢出。

//注意保存的是前一个request,移除的却是当前request,因为二者引用的是同一个session,内部只要找到这个session,移除对应的缓存key即可

requestCache.removeRequest(request, response);

super.onAuthenticationSuccess(request, response, authentication);

return;

}

//4、移除在session中缓存的之前的登录错误信息,key为:SPRING_SECURITY_LAST_EXCEPTION

clearAuthenticationAttributes(request);

//5、跳转到之前保存的request对象中访问的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;

}

}

登录失败

在AbstractAuthenticationFilterConfigurer中,failureHandler字段值默认为空,在初始化时,updateAuthenticationDefaults方法会被调用:

AbstractAuthenticationFilterConfigurer#updateAuthenticationDefaults....

private AuthenticationFailureHandler failureHandler;

....

private void updateAuthenticationDefaults() {

....

if (failureHandler == null) {

failureUrl(loginPage + "?error");

}

....

}

可以看到,如果发现failureHandler为空,则会调用failureUrl方法创建一个AuthenticationFailureHandler实例,传入的参数是是我们设置的loginPage+"?ERROR",这也是我们在前面的gif动态图中,看到登录失败之后,登录页面变为http://localhost:8080/login?error的原因。

failUrl方法如下所示:public final T failureUrl(String authenticationFailureUrl) {

T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(

authenticationFailureUrl));

this.failureUrl = authenticationFailureUrl;

return result;

}

可以看到这里创建的AuthenticationFailureHandler实现类为SimpleUrlAuthenticationFailureHandler。

3 总结

本节涉及的类比较多,这里再简单总结一下,以便读者理解:

FormLoginConfigurer是AbstractAuthenticationFilterConfigurer的子类。

UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter的子类。

FormLoginConfigurer用于创建UsernamePasswordAuthenticationFilter,参见FormLoginConfigurer构造方法:public FormLoginConfigurer() {

super(new UsernamePasswordAuthenticationFilter(), null);

usernameParameter("username");

passwordParameter("password");

}

首先创建了一个UsernamePasswordAuthenticationFilter,然后调用父类AbstractAuthenticationFilterConfigurer的构造方法,AbstractAuthenticationFilterConfigurer中通过authFilter字段来维护UsernamePasswordAuthenticationFilter。protected AbstractAuthenticationFilterConfigurer(F authenticationFilter,

String defaultLoginProcessingUrl) {

this.authFilter = authenticationFilter;

setLoginPage("/login");

if (defaultLoginProcessingUrl != null) {

loginProcessingUrl(defaultLoginProcessingUrl);

}

}

我们通过formLogin方法获得FormLoginConfigurer对象,对其进行的的各种配置实际上大多都是直接或者间接的设置到了AbstractAuthenticationFilterConfigurer中:.formLogin() //内部注册 UsernamePasswordAuthenticationFilter

.loginPage("/login.html")

.loginProcessingUrl("/admin/login")

.usernameParameter("username")

.passwordParameter("password")

.successForwardUrl("/success.html")

.failureForwardUrl("/error.html")

spring security在初始化时,会回调AbstractAuthenticationFilterConfigurer的configure方法,此时这些配置才会被真正的设置到UsernamePasswordAuthenticationFilter或者其父类AbstractAuthenticationProcessingFilter中,之后UsernamePasswordAuthenticationFilter就可以直接使用这些配置进行工作。

在登录成功或者失败之后,需要进行一些页面跳转的工作,AbstractAuthenticationProcessingFilter维护了两个字段,分别用于成功和失败的跳转:private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

其中AuthenticationSuccessHandler用于登录成功的跳转,默认实现为SavedRequestAwareAuthenticationSuccessHandler

AuthenticationFailureHandler用于登录失败的跳转,默认实现为SimpleUrlAuthenticationFailureHandler

当我们调用successForwardUrl、failureForwardUrl方法时,默认值会分别覆盖为:ForwardAuthenticationSuccessHandler、ForwardAuthenticationFailureHandler,此时跳转都是转发的方式,用户浏览器地址栏url不会改变。

AuthenticationSuccessHandler接口类图继承关系如下:

f10b7fc0819316b9dea26f12bcdfde8e.png

AuthenticationFailureHandler接口类图继承关系如下:

18fcacb90a741d4785e45608bf033873.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值