用户登录
跳到主要内容

10、Spring Security 实战 - 传统Web项目表单认证: UsernamePasswordAuthenticationFilter 过滤器

第一步

①自定义登录页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
</head>
<body>

<h1>用户登录</h1>
<h2>
    <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h2>
<form method="post" th:action="@{/doLogin}">
    用户名: <input name="uname" type="text"> <br>
    密码: <input name="passwd" type="text"> <br>
    <input type="submit" value="登录">
</form>

</body>
</html>

②登录页面的控制器

@Controller
public class LoginController {
   
     
    @RequestMapping("/login.html")
    public String login() {
   
     
        return "login";
    }
}

③SpringSecurity配置类

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                // 放行访问登录页面的/login.html请求
                .mvcMatchers("/login.html").permitAll()
                // 放行/index请求
                .mvcMatchers("/index").permitAll()
                // 其他所有的请求都需要去认证
                .anyRequest().authenticated()
                .and()
                // 认证方式为表单认证
                .formLogin()
                    // 指定默认的登录页面
                    .loginPage("/login.html")
                    // 指定登录请求路径
                    .loginProcessingUrl("/doLogin")
                    // 指定表单用户名的 name 属性为 uname
                    .usernameParameter("uname")
                    // 指定表单密码的 name 属性为 passwd
                    .passwordParameter("passwd")
                    // 指定登录成功后的自定义处理逻辑
                    .defaultSuccessUrl("/index")
                .and()
                // 禁止csrf跨站请求保护
                .csrf().disable();
    }
}

④控制器

@Slf4j
@RestController
public class HelloController {
   
     
    @RequestMapping("/hello")
    public String hello() {
   
     
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("身份信息:{}",authentication.getPrincipal());
        log.info("权限信息:{}",authentication.getAuthorities());
        return "hello security";
    }
}

启动项目,访问 localhost:8080/hello,跳转到登录页面,打上断点,点击登录:

 

第二步

当在登录表单中输入用户名和密码点击登录后,登录请求会进行AbstractAuthenticationProcessingFilte过滤器的doFilter方法,AbstractAuthenticationProcessingFilte 作为身份认证请求入口,是一个抽象类。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都继承了 AbstractAuthenticationProcessingFilter ,并重写了方法 attemptAuthentication 进行身份认证。

 

AbstractAuthenticationProcessingFilte源码:

首先判断登录页面中配置的th:action="@{/doLogin}"请求路径是否是我们在WebSecurityConfigurer表单登录中配置的loginProcessingUrl("/doLogin")相同。如果相同则尝试调用子类的attemptAuthentication方法尝试认证。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
   
     
        // 1、判断登录页面中配置的action请求路径是否是我们在WebSecurityConfigurer表单登录中配置的loginProcessingUrl相同
		if (!requiresAuthentication(request, response)) {
   
     
			chain.doFilter(request, response);
			return;
		}
		try {
   
     
            // 2、调用子类的实现尝试认证
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
   
     
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// 3、Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
   
     
				chain.doFilter(request, response);
			}
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
   
     
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
   
     
			// 4、Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}
	}
}

第三步

调用子类UsernamePasswordAuthenticationFilter#attemptAuthentication方法,判断方法是否是post方法,根据请求参数uname,passwd获取登录用户名username和密码password,然后将需要做认证的username和password封装成Authentication对象UsernamePasswordAuthenticationToken,交给AuthenticationManager接口的子类ProviderManager去认证。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   
     

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
   
     
        // 判断请求方式是否为post请求
        if (this.postOnly && !request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 从请求中获取登录用户名username
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        // 从请求中获取登录密码password
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        // 将username和password封装成Authentication对象UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        // 将Authentication对象交给AuthenticationManager接口子类的authenticate方法尝试认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
    @Nullable
	protected String obtainPassword(HttpServletRequest request) {
   
     
		return request.getParameter(this.passwordParameter);
	}

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
   
     
		return request.getParameter(this.usernameParameter);
	}
}

第四步

Authentication对象ProviderManager#authenticate方法尝试认证。

在Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供

多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider列表,在ProviderManager中遍历列表中的每⼀个AuthenticationProvider去执⾏身份认证,最终得到认证结果。

ProviderManager源码核心流程:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
   
     
    // AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
	private AuthenticationManager parent;
    
    @Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   
     
		Class<? extends Authentication> toTest = authentication.getClass();
		Authentication result = null;
        Authentication parentResult = null;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
   
     
			if (!provider.supports(toTest)) {
   
     
				continue;
			}
			try {
   
     
				result = provider.authenticate(authentication);
			}
            // ...
		}
		if (result == null && this.parent != null) {
   
     
			// Allow the parent to try.
			try {
   
     
                // 调用父类的ProviderManager进行认证
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
            // ...
		}
	}
}

默认情况下,ProviderManager的AuthenticationProvider列表中包含一个实现类:AnoymousAuthenticationProvider,因此for循环内遍历得到AnoymousAuthenticationProvider,执行AnonymousAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果不支持,代码如下:

public class AnonymousAuthenticationProvider implements AuthenticationProvider,
      MessageSourceAware {
   
     

   public boolean supports(Class<?> authentication) {
   
     
      return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
   }
}

跳出for循环,继续调用父类的ProviderManager进行认证,回调ProviderManager#authenticate方法,此时父类ProviderManager的AuthenticationProvider列表中有一个默认类DaoAuthenticationProvider。该类继承自AbstractUserDetailsAuthenticationProvider类,会调用AbstractUserDetailsAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果支持。

public abstract class AbstractUserDetailsAuthenticationProvider implements
    AuthenticationProvider, InitializingBean, MessageSourceAware {
   
     
    
    public boolean supports(Class<?> authentication) {
   
     
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }
}

因此 result = provider.authenticate(authentication) 最终会调用AbstractUserDetailsAuthenticationProvider#authenticate方法对UsernamePasswordAuthenticationToken对象完成认证,在该方法中根据username获取数据源中存储的用户user,然后判断user是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。