上一节我们跟踪了security的默认登录页的源码,可以参考这里:https://www.cnblogs.com/process-h/p/15522267.html 这节我们来看看如何自定义单表认证页及源码跟踪。
为了实现自定义表单及登录页,我们需要编写自己的WebSecurityConfig
类,继承了WebSecurityConfigurerAdapter
对象,通过重写configure
方法,定义自己的登录页路径及失败跳转的路径。
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authorizeRequests -> authorizeRequests .antMatchers("/css/**", "/index").permitAll() .antMatchers("/user/**").hasRole("USER") ) .formLogin(formLogin -> formLogin .loginPage("/login") .failureUrl("/login-error") ); } // @formatter:on @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } }
我们通过引入Thymeleaf
模板来实现跳转
@Controller public class MainController { @RequestMapping("/") public String root() { return "redirect:/index"; } @RequestMapping("/index") public String index() { return "index"; } @RequestMapping("/user/index") public String userIndex() { return "user/index"; } @RequestMapping("/login") public String login() { return "login"; } @RequestMapping("/login-error") public String loginError(Model model) { model.addAttribute("loginError", true); return "login"; } }
上一节我们提到了WebSecurityConfig
类,它会有一个init
方法
@Override public void init(WebSecurity web) throws Exception { HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http).postBuildAction(() -> { FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); }); }
这里提到了HttpSecurity
对象,顾名思义,它的作用就是保证Http请求的安全,那么它是如何保证http请求的安全的呢?我们来看看getHttp()方法
protected final HttpSecurity getHttp() throws Exception { if (this.http != null) { return this.http; } // 初始化认证事件发布者,也就是定义了一些异常跟异常事件类之前的映射关系 AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher(); this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); // 初始化认证管理者 AuthenticationManager authenticationManager = authenticationManager(); this.authenticationBuilder.parentAuthenticationManager(authenticationManager); Map<Class<?>, Object> sharedObjects = createSharedObjects(); this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects); if (!this.disableDefaults) { // 默认情况下会去加载配置 applyDefaultConfiguration(this.http); ClassLoader classLoader = this.context.getClassLoader(); List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader .loadFactories(AbstractHttpConfigurer.class, classLoader); for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) { this.http.apply(configurer); } } configure(this.http); return this.http; } // 默认认证事件发布者 public DefaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; addMapping(BadCredentialsException.class.getName(), AuthenticationFailureBadCredentialsEvent.class); addMapping(UsernameNotFoundException.class.getName(), AuthenticationFailureBadCredentialsEvent.class); addMapping(AccountExpiredException.class.getName(), AuthenticationFailureExpiredEvent.class); addMapping(ProviderNotFoundException.class.getName(), AuthenticationFailureProviderNotFoundEvent.class); addMapping(DisabledException.class.getName(), AuthenticationFailureDisabledEvent.class); addMapping(LockedException.class.getName(), AuthenticationFailureLockedEvent.class); addMapping(AuthenticationServiceException.class.getName(), AuthenticationFailureServiceExceptionEvent.class); addMapping(CredentialsExpiredException.class.getName(), AuthenticationFailureCredentialsExpiredEvent.class); addMapping("org.springframework.security.authentication.cas.ProxyUntrustedException", AuthenticationFailureProxyUntrustedEvent.class); addMapping("org.springframework.security.oauth2.server.resource.InvalidBearerTokenException", AuthenticationFailureBadCredentialsEvent.class); }
我们来看看applyDefaultConfiguration
这个方法,在上一节有讲到,这里是给httpSecurity对象配置一些默认的配置,比如默认会开启csrf跨站请求伪造防护,添加WebAsyncManagerIntegrationFilter
过滤器,添加默认的登录页配置DefaultLoginPageConfigurer
等。
private void applyDefaultConfiguration(HttpSecurity http) throws Exception { http.csrf(); http.addFilter(new WebAsyncManagerIntegrationFilter()); http.exceptionHandling(); http.headers(); http.sessionManagement(); http.securityContext(); http.requestCache(); http.anonymous(); http.servletApi(); http.apply(new DefaultLoginPageConfigurer<>()); http.logout(); }
回到调用applyDefaultConfiguration()
的主方法这里,执行完if (!this.disableDefaults) {}
分支之后,会调用自身的configure(this.http);
方法,也就是我们自定义的WebSecurityConfig
类中重写的方法,会去执行我们的表单登录配置策略。
.formLogin(formLogin -> formLogin .loginPage("/login") .failureUrl("/login-error") ); @Override public FormLoginConfigurer<H> loginPage(String loginPage) { return super.loginPage(loginPage); } protected T loginPage(String loginPage) { setLoginPage(loginPage); updateAuthenticationDefaults(); this.customLoginPage = true; return getSelf(); }
点击.loginPage("/login")
方法,再点击super.loginPage(loginPage);
可以看到登录页已经被重写了,自定义登录页标志也被写成了true。
自定义表单登录页及源码跟踪就到这里,过程中还发现了跟security最为密切的filter顺序定义,在该FilterOrderRegistration
类的构造方法中,定义了security中可能会用到的所有filter的顺序,有兴趣的读者自行阅读下。登录相关的源码跟的线条比较粗,接下来该看看认证跟授权的部分了。