本篇介紹Spring Security預設的登入及登出頁面(default login logout page)是在哪裡被產生的。
簡單說Spring Security預設登入登出頁面其實是由後端產生,而非一個預先建立好的html頁面。
預設登入頁面由DefaultLoginPageGeneratingFilter
產生。
預設登出頁面由DefaultLogoutPageGeneratingFilter
產生。
Spring Security在初始時會呼叫WebSecurityConfigurerAdapter.init()
,接著在裡面
呼叫WebSecurityConfigurerAdapter.getHttp()
建立並執行HttpSecurity
的預設配置。
在進行HttpSecurity
的預設配置時套用了DefaultLoginPageConfigurer
。
節錄WebSecurityConfigurerAdapter
原始碼如下,
執行順序為init() -> getHttp() -> configure()
。
WebSecurityConfigurerAdapter
/** 此即為自訂Spring Security配置類別會繼承的抽像類別 */
@Order(100)
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
...
/** 取得HttpSecurity物件及預設配置 */
@SuppressWarnings({ "rawtypes", "unchecked" })
protected final HttpSecurity getHttp() throws Exception {
...
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects); // 建立HttpSecurity物件
if (!disableDefaults) { // disableDefaults預設是false,所以繼續進行下面的HttpSecurity配置
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and() // 建構並套用DefaultLoginPageConfigurer,在此生成預設的登入及登出頁面。
.logout();
...
}
configure(http); // 其餘的HttpSecurity預設配置
return http;
}
...
/** WebSecurityConfigurerAdapter初始化 */
public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp(); // 取得HttpSecurity物件及預設配置
...
}
/** 其餘的HttpSecurity預設配置 */
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
DefaultLoginPageConfigurer
建構時即載入了兩個成員變數DefaultLoginPageGeneratingFilter
及DefaultLogoutPageGeneratingFilter
的實例,也就是Spring Security預設的登出及登入頁面的來源。
節錄DefaultLoginPageConfigurer
原始碼如下。
DefaultLoginPageConfigurer
public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<DefaultLoginPageConfigurer<H>, H> {
private DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = new DefaultLoginPageGeneratingFilter(); // 在這產生預設登入頁面
private DefaultLogoutPageGeneratingFilter logoutPageGeneratingFilter = new DefaultLogoutPageGeneratingFilter(); // 在這產生預設登出頁面
@Override
public void init(H http) throws Exception {
Function<HttpServletRequest, Map<String, String>> hiddenInputs = request -> {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (token == null) {
return Collections.emptyMap();
}
return Collections.singletonMap(token.getParameterName(), token.getToken());
};
this.loginPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); // 設定預設登入頁面隱藏的csrf input欄位<input name="_csrf" type="hidden" value="${_csrf.token}">
this.logoutPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); // 設定預設登出頁面隱藏的csrf input欄位<input name="_csrf" type="hidden" value="${_csrf.token}">
http.setSharedObject(DefaultLoginPageGeneratingFilter.class,
loginPageGeneratingFilter);
}
@Override
@SuppressWarnings("unchecked")
public void configure(H http) throws Exception {
AuthenticationEntryPoint authenticationEntryPoint = null;
ExceptionHandlingConfigurer<?> exceptionConf = http
.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionConf != null) {
authenticationEntryPoint = exceptionConf.getAuthenticationEntryPoint();
}
if (loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) {
loginPageGeneratingFilter = postProcess(loginPageGeneratingFilter);
http.addFilter(loginPageGeneratingFilter); // 把預設登入頁面加入HttpSecuirty的filters
http.addFilter(this.logoutPageGeneratingFilter); // 把預設登出頁面加入HttpSecuirty的filters
}
}
}
每次客戶端發出請求時都須先通過Spring Security的Filter過濾,如果尚未驗證便會重新導向預設的登入頁面。
預設登入頁面的url為/login
,並在通過Spring Security的FilterChainProxy
時呼叫DefaultLoginPageGeneratingFilter.doFilter()
並呼叫私有方法DefaultLoginPageGeneratingFilter.generateLoginPageHtml()
產生登入頁面html字串最終透過HttpServletResponse
寫出並渲染為html頁面。
節錄DefaultLoginPageGeneratingFilter
原始碼如下。
DefaultLoginPageGeneratingFilter
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess); // 產生預設的登入頁面html字串
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml); // 透過HttpServletResponse寫出登入頁面html字串
return;
}
chain.doFilter(request, response);
}
...
/** 產生預設登入頁面html字串 */
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
boolean logoutSuccess) {
...
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n"
+ "<html lang=\"en\">\n"
+ " <head>\n"
+ " <meta charset=\"utf-8\">\n"
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
+ " <meta name=\"description\" content=\"\">\n"
+ " <meta name=\"author\" content=\"\">\n"
+ " <title>Please sign in</title>\n"
+ " <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"
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
+ " </head>\n"
+ " <body>\n"
+ " <div class=\"container\">\n");
String contextPath = request.getContextPath();
if (this.formLoginEnabled) {
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
+ " <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
+ createError(loginError, errorMsg)
+ createLogoutSuccess(logoutSuccess)
+ " <p>\n"
+ " <label for=\"username\" class=\"sr-only\">Username</label>\n"
+ " <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
+ " </p>\n"
+ " <p>\n"
+ " <label for=\"password\" class=\"sr-only\">Password</label>\n"
+ " <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"
+ " </p>\n"
+ createRememberMe(this.rememberMeParameter)
+ renderHiddenInputs(request)
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
+ " </form>\n");
}
...
sb.append("</div>\n");
sb.append("</body></html>");
return sb.toString();
}
...
}
登出頁面由DefaultLogoutPageGeneratingFilter
的產生過程相同,就不另外說明了。
參考:
沒有留言:
張貼留言