第一步
①自定义登录页面
<!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是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。