上一節我們跟蹤了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的順序,有興趣的讀者自行閱讀下。登入相關的原始碼跟的線條比較粗,接下來該看看認證跟授權的部分了。