一文上手SpringSecurity【二】
书接上回,我们直接引入了spring security的依赖,之后啥也没有干,在访问接口的时候, 就需要认证之后才能访问了 ,咱们没有主动干啥,那肯定有人帮助我们干啥了,这一切都利益出spring boot自动装配机制,下面咱们就看看spring security的自动装配,帮助我们干啥了.
一、Spring Security自动装配
1.1 回顾一下spring boot的自动装配
spring boot会根据类路径中的jar包、类,为jar包里的类自动配置,这样可以极大的减少配置的数量。也就是说spring boot会根据定义在classpath下的类,自动的给你生成一些Bean,并加载到Spring的Context中。
spring boot通过条件注解的配置决定哪些bean可以被自动装配,这些条件定义成具体的xxxAutoConfiguration, 将xxxAutoConfiguration配置到spring.factories文件当中,最好基于spi机制进行加载.但是需要注意的是在spring boot 2.7.0版本及之后的版本,此种方式已经不被推荐使用了,而是加载:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, 如下图所示:
文件内容:
// 部分内容 ....
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration
其核心注解@Import(AutoConfigurationImportSelector.class)读取类路径下的文件需要自动装配的类, 一般都是以xxxAutoConfiguration结尾, 再加载的时候,结合条件注解,过滤一下哪些个bean需要加载.以WebMvcAutoConfiguration为例进行说明.
当读取到了WebMvcAutoConfiguration这个类,开始加载所对应的bean.
同样在WebMvcAutoConfiguration类当中,观察一下WebMvcAutoConfigurationAdapter类,如下图所示:
- 重要的小总结
- 读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,加载各种各样的Bean
- 结合条件注解@Condition,决定加载哪些、不加载哪些Bean
- 一般自动装配的类都是以xxxAutoConfigufation结尾
- 一般默认属性配置类都会在xxxAutoConfiguration类当中引入,以xxxProperties结尾
以Redis自动装配为例,再体会一下.目前我们的工程当中没有引入spring-boot-starer-data-redis依赖,但是在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件当中,有它的自动装配类,如下图所示:
打开瞅瞅
有了以上的知识作为铺垫,我们直接来看spring security自动装配.
1.2 spring security自动装配
1.2.1 @Conditional(DefaultWebSecurityCondition.class)注解
按照我们之前说的,一般都会有xxxAutoConfiguration,我们直接搜索一下瞅瞅. 费劲扒拉的搜了半天,发现直接搜索SpringSecurityAutoConfiguration没有,得搜SecurityAutoConfiguration才能找到.
@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
@EnableConfigurationProperties(SecurityProperties.class),表示启用默认装配的各种属性.
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class }), 表示引入其它的配置.我们看SpringBootWebSecurityConfiguration.class这个类. 核心代码如下所示:
// Web 安全性的默认配置。它依赖于 Spring Security 的内容协商策略来确定要使用的身份验证类型。
// 如果用户指定了自己的 SecurityFilterChain bean,这将完全回退,
// 用户应该指定他们想要作为自定义安全配置的一部分配置的所有位。
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
@ConditionalOnDefaultWebSecurity注解用于条件化地配置 Web 安全相关的组件,只有在满足特定条件时才会生效. 其核心代码如下所示:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultWebSecurityCondition.class)
public @interface ConditionalOnDefaultWebSecurity {
}
@Conditional(DefaultWebSecurityCondition.class),这个注解告诉 Spring,只有当DefaultWebSecurityCondition中定义的条件满足时,才会应用被注解的配置类、方法或 Bean. 终于到关键代码了:
class DefaultWebSecurityCondition extends AllNestedConditions {
DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
@ConditionalOnMissingBean({ SecurityFilterChain.class })
static class Beans {
}
}
下边重点解释一下这 DefaultWebSecurityCondition类. 重点就看这两个静态内部类:
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
- @ConditionalOnClass 是一个 Spring Boot 的条件注解。它的意思是:只有在类路径上存在指定的类(SecurityFilterChain 和 HttpSecurity)时,条件才成立。
- 这里指定了两个类 SecurityFilterChain 和 HttpSecurity,它们属于 Spring Security 的核心类。只有当这些类可用时,Classes 条件才会被满足。
@ConditionalOnMissingBean({ SecurityFilterChain.class })
static class Beans {
}
- @ConditionalOnMissingBean 也是一个条件注解。它的意思是:如果 Spring 容器中不存在指定的 bean(SecurityFilterChain),条件才会成立。
- 这个注解确保了当 SecurityFilterChain bean 不存在时,Spring 可以根据此条件加载其他配置。换句话说,它防止了重复创建 SecurityFilterChain 的 bean。
综上所述, 得出结论, 使得DefaultWebSecurityCondition的生效条件有两个:
- 类路径中必须存在 SecurityFilterChain 和 HttpSecurity(由 Classes 内部类定义)
- pring 容器中不能已经存在 SecurityFilterChain bean(由 Beans 内部类定义)
也就是说,如果我们需要自己定义spring security相关配置,只需要破坏这两个条件当中的一个即可
1.2.2 默认SecurityFilterChain配置
由于现在我们并没有自定义配置,所以现在生效的都是默认配置,回到SpringBootWebSecurityConfiguration类当中,查看生效的默认配置.
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity // 这个注解生效了,则直接开始加载spring security默认配置
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 第1行
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
// 第2行
http.formLogin(withDefaults());
// 第3行
http.httpBasic(withDefaults());
return http.build();
}
}
- 第1行代码: 配置了请求授权规则, requests.anyRequest().authenticated()表示应用程序的所有请求都需要经过身份认证,也就是说,任何请求都不能匿名访问,必须是已登录的用户。
- 第2行代码: 启用了默认的表单登录(form-based login), 就是咱们在一文上手SpringSecurity一当中看到的那个表单.
- 第3行代码:启用了默认的HTTP Basic 认证, 这是一种简单的基于用户名和密码的认证机制,通常用于 API 或没有复杂前端的应用中。
- 【方法入参】: HttpSecurity是配置 HTTP 安全的重要构建器类, 由spring 提供.
- 【方法出参】: SecurityFilterChain对象, 该对象用于构建和配置安全过滤器链, 整个spring security由n个过滤器组成的
经过以上的分析,可以得出出下的结论:
- spring security执行了默认配置
- 默认配置当中,配置了默认的授权规则、默认的登录表单
- SecurityFilterChain用于构建过滤器链,整个spring security由多个过滤器链组成
- HttpSecurity核心的构建器类,spring security各种功能都是由它来配置
那么我进一步探讨一个,默认表单是如何生成的,默认的用户名称和密码又是如何创建的呢?
二、默认流程处理
2.1 默认登录页面流程
我们之前看到的SecurityFilterChain接口,它表示过滤器链, 当请求到达服务器的时候,会经过过滤器链,看一眼接口定义:
public interface SecurityFilterChain {
// 匹配请求
boolean matches(HttpServletRequest request);
// 用户存储Filter,即javaee当中的过滤器对象
List<Filter> getFilters();
}
其实现类:
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
// ...
根据上边说的自动装配,应用程序启动的时候,会自动创建SecurityFilterChain对象,将其放到容器当中,所以我们可以在应用程序的入口,从容器当中取出List,查看一下到底有哪些过滤器.
public class RjSpringSecurityDemo01Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(RjSpringSecurityDemo01Application.class, args);
DefaultSecurityFilterChain filterChain = context.getBean(DefaultSecurityFilterChain.class);
List<Filter> filters = filterChain.getFilters();
for (Filter filter : filters) {
System.out.println(filter.getClass().getName());
}
}
}
日志内容如下所示
org.springframework.security.web.session.DisableEncodeUrlFilter
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextHolderFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.AuthorizationFilter
稍微看一眼,上边输出的过滤器,这些过滤器共同组成了一个过滤器链,请求打到服务器的时候,依次经过这些过滤器的处理. 从多的过滤器当中,咱们光从字面意思来看,就找咱们认识的,应该能找到:
- LogoutFilter, 登出过滤器
- UsernamePasswordAuthenticationFilter, 用户名称和密码认证过滤器
- DefaultLoginPageGeneratingFilter, 默认的登录页面生成过滤器,【哎哟可以哦,好像就是它啊】
- DefaultLogoutPageGeneratingFilter, 默认的登出页面生成过滤器
- ExceptionTranslationFilter, 异常处理过滤器
- AuthorizationFilter, 授权过滤器
- …
处理流程:
- 当我们请求/hello接口的时候,由于我们引入了spring security,所以当前的这个请求会经过一个又一个的过滤器
- 当请求到达AuthorizationFilter过滤器的时候,发现该请求并未认证.请求会被拦截下来,并且抛出
AccessDeniedExceptin
异常. - 当抛出异常之后会被过滤器
ExceptionTranslationFilter
捕获,这个Filter中会调用LoginUrlAuthenticationEntryPoint#commence
方法给客户端返回302,要求客户端重写向到/login页面 - 客户端发送请求到/login
- /login会再次被拦截器
DefaultLoginPageGeneratingFilter
拦截到,并且在该拦截器当中生成登录在页面.
核心源码:
DefaultLoginPageGeneratingFilter过滤器当中
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
public static final String DEFAULT_LOGIN_PAGE_URL = "/login";
// ...
}
doFilter方法
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 生成默认的登录页面
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
generateLoginPageHtml, 生成页面方法
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
String errorMsg = loginError ? getLoginErrorMessage(request) : "Invalid credentials";
String contextPath = request.getContextPath();
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html lang=\"en\">\n");
sb.append(" <head>\n");
sb.append(" <meta charset=\"utf-8\">\n");
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
sb.append(" <meta name=\"description\" content=\"\">\n");
sb.append(" <meta name=\"author\" content=\"\">\n");
sb.append(" <title>Please sign in</title>\n");
sb.append(" <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" "
+ "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" "
+ "rel=\"stylesheet\" integrity=\"sha384-oOE/3m0LUMPub4kaC09mrdEhIc+e3exm4xOGxAmuFXhBNF4hcg/6MiAXAf5p0P56\" crossorigin=\"anonymous\"/>\n");
sb.append(" <
// ... 其它的略,自己去源码查看即可
}
2.2 默认认证流程分析
从自动装配的类,找到登录表单的默认配置
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults()); // 默认表单配置
http.httpBasic(withDefaults());
return http.build();
}
点进方法
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
FormLoginConfigurer<>()方法
public FormLoginConfigurer() {
// 调用父类过滤器
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
进入UsernamePasswordAuthenticationFilter(),找到UsernamePasswordAuthenticationFilter#构造方法
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
得到常量值:
// 表示登录的请求地址和请求姿势
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
构造方法内部调用了父类的构造方法,进入到UsernamePasswordAuthenticationFilter父类AbstractAuthenticationProcessingFilter当中,由于是过滤器,实现了接口Filter,所以必然会执行doFilter方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 调用本类的成员方法
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
UsernamePasswordAuthenticationFilte#当前类的doFiler()
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 在AbstractAuthenticationProcessingFilter类定义的【模板方法】attemptAuthentication(), 尝试认证, 交给它的子类UsernamePasswordAuthenticationFilte去实现.
Authentication authenticationResult = attemptAuthentication(request, response);
// 略....
}
AbstractAuthenticationProcessingFilter类定义的【模板方法】attemptAuthentication()
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
UsernamePasswordAuthenticationFilte实现父类AbstractAuthenticationProcessingFilter定义的模板方法attemptAuthentication(),【这就到了认证的核心逻辑】
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 取出用户名称和密码
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 将用户名称和密码封装为UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 认证管理器调用认证方法,即去验证我们输入的用户名称和密码是否正确这件事
return this.getAuthenticationManager().authenticate(authRequest);
}
this.getAuthenticationManager().authenticate(authRequest)这一块比较复杂,包括两个内容:
- this.getAuthenticationManager(),返回认证管理器对象AuthenticationManager
- authenticate(), 接口AuthenticationManager定义认证方法
// 认证管理器接口
public interface AuthenticationManager {
// 【核心认证方法】, 可以发现,此处的入参跟出参都是Authentication接口,如果认证失败,抛出认证异常,如果认证成功,则
// 返回一个完整的Authentication对象.
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager接口的实现类有
那么问题来了,接口不能实例化,那么此时this.getAuthenticationManager()指向了它的哪个实现类呢?这里如果有兴趣的伙伴,可以去自己debug一下,我直接说了哈: 使用的是: ProviderManager这个实现类,通过ProviderManager实现类的对象去调用认证方法.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// ...
}
ProviderManager当中重写接口AuthenticationManager的authenticate()方法【核心方法实现】
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 略去了其它代码....
for (AuthenticationProvider provider : getProviders()) {
try {
// 核心方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
}
}
AuthenticationProvider接口表示认证提供的接口,它的定义和AuthenticationManager一样
public interface AuthenticationProvider {
// 认证方法和认证管理器AuthenticationManager一毛一样
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider是一个接口,不能直接使用,必须在运行时找它一个它的实现类,然后调用认证方法,默认指向的实现类是DaoAuthenticationProvider,由它实现在AuthenticationProvider接口定义的认证方法, 但是你查看它的类定义如下所示:
// 实现类并没有直接实现接口,而在父类当中实现的接口,在此类查找authenticate()的实现发现向了父类,在父当中实现的抽象方法
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
// ...
}
AbstractUserDetailsAuthenticationProvider
// 实现了接口AuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
// ...
}
AbstractUserDetailsAuthenticationProvider#authenticate()核心逻辑
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 这里也是使用了【模板】方法,在当前类AbstractUserDetailsAuthenticationProvider定义的模板方法,具体功能
// 由子类DaoAuthenticationProvider去实现
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
}
AbstractUserDetailsAuthenticationProvider#retrieveUser()模板方法定义
// 检索用户
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
AbstractUserDetailsAuthenticationProvider子类DaoAuthenticationProvider实现父类定义的模板方法retrieveUser
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 核心代码
// 根据用户名称去数据源当中查找出用户信息,并将用户对象封装为UserDetails
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
this.getUserDetailsService().loadUserByUsername(username);
- this.getUserDetailsService(), 返回接口UserDetailsService, 即用户详情服务.
接口定义
public interface UserDetailsService {
// 根据用户名,去数据源当中查询出用户信息,并将认证的用户信息封装成UserDetails对象
// 返回值: UserDetails是一个接口,主要包含三个东西: 用户名称、密码、权限列表
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
this.getUserDetailsService()在运行时,返回一个实现了UserDetailsService接口的实现类的对象.这里返回的是: InMemoryUserDetailsManager
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
// ...
}
InMemoryUserDetailsManager#loadUserByUsername()
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名称,取出用户信息,并且封装成了UserDetails对象
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
// User是个实现了接口UserDetails的实现类
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
最返回信息UserDetails .
至此认证流程基本分析完毕.剩下的是验证用户名称和密码的过程、存储缓存等步骤了.下篇再进行分析.
大家初看一大堆接口和实现类,还有各种模板方法,不必慌张,整个流程还是比较清晰的,建议先跟着文章,自己动手实践一下.多看几次也就记住了.这玩意没有捷径,我亦无他,惟手熟尔矣.
三、总结
3.1 重点内容总结
- spring boot 3.3.4 自动装配核心内容
- spring security 过滤器链
- spring security自动装配
- spring security默认流程处理之默认的登录页面处理流程
- spring security 认证流程分析
3.2 下篇内容
- spring security认证流程图解
- 默认密码生成策略
- 自定义认证页面和密码