SpringSecurity Initialization Process Source

This article mainly explains the source part of the SpringSecurity initialization process, including how the core springSecurityFilterChain was created and where personalized configurations can be extended. The SpringSecurity source code is a real headache for all kinds of Builder Configurations!

_1. Brief introduction

_The core functions of SpringSecurity include:

_Certification (who are you) _Authorization (what can you do) _Attack protection (to prevent forgery of identity)

_Its core is a set of filter chains, which will be automatically configured after the project is started. This article will also cover how the filter chains are automatically initialized.

_SecurityContextPersistenceFilter is the first filter _When requested, it checks to see if there is a SecurityContext in the session based on the sessionId found in the session, then puts the SecurityContext in the current thread _When responding, check to see if the current thread has a SecurityContext, and if you put it in a session so that different requests can get the same user authentication information.

_UsernamePasswordAuthenticationFilter This filter processes form logins, which process authentication submitted through form logins

_SocialAuthenticationFilter such as this is the Filter used by social logins _See my other SpringSocial article for third-party QQ login in more detail SpringSocial implements third-party QQ login

_Green filters are configurable, not any other color!

 2.SecurityAutoConfiguration

_If it is a SpringBoot project, there will still be auto-configuration classes as long as you depend on SpringSecurity-related dependencies SecurityAutoConfiguration takes effect and it imports WebSecurityEnableConfiguration

@EnableWebSecurity will be our main starting point for this article

_3.@EnableWebSecurity annotation introduction

_This annotation is the entry to initialize Spring Security.

_Open the @EnableWebSecurity comment

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
	SpringWebMvcImportSelector.class,
	OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

/**
 * Controls debugging support for Spring Security. Default is false.
 * @return if true, enables debug support with Spring Security
 */
boolean debug() default false;
}

_This annotation class introduces a configuration class (WebSecurityConfiguration) and two ImportSelectors (Spring WebMvcImportSelector, OAuth2ImportSelector) using @Configuration with @Import, and we focus on WebSecurityConfiguration, which is the core of Spring Security

_4.springSecurityFilterChain initialization process and source code

_Open WebSecurityConfiguration It is a configuration class, mainly looking at the springSecurityFilterChain() method, which is initialization Core method of springSecurityFilterChain

/**
 * Creates the Spring Security Filter Chain
 * @return the {@link Filter} that represents the security filter chain
 * @throws Exception
 */
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
	boolean hasConfigurers = webSecurityConfigurers != null
			&& !webSecurityConfigurers.isEmpty();
	if (!hasConfigurers) {
		WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
				.postProcess(new WebSecurityConfigurerAdapter() {
				});
		webSecurity.apply(adapter);
	}
	return webSecurity.build();
}

@Bean annotation name attribute value AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME is the springSecurityFilterChain defined in XML

_From the source code, you know that the filter was created by the last webSecurity.build(), which is of the type WebSecurity, which was created first in the setFilterChainProxySecurityConfigurer method:

@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
		ObjectPostProcessor<Object> objectPostProcessor,
		@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
		throws Exception {
	webSecurity = objectPostProcessor
			.postProcess(new WebSecurity(objectPostProcessor));
	if (debugEnabled != null) {
		webSecurity.debug(debugEnabled);
	}

	webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);

	Integer previousOrder = null;
	Object previousConfig = null;
	for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
		Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
		if (previousOrder != null && previousOrder.equals(order)) {
			throw new IllegalStateException(
					"@Order on WebSecurityConfigurers must be unique. Order of "
							+ order + " was already used on " + previousConfig + ", so it cannot be used on "
							+ config + " too.");
		}
		previousOrder = order;
		previousConfig = config;
	}
	for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
		webSecurity.apply(webSecurityConfigurer);
	}
	this.webSecurityConfigurers = webSecurityConfigurers;
}

_As you can see from the code, it comes out directly from new er:

webSecurity = objectPostProcessor
			.postProcess(new WebSecurity(objectPostProcessor));

_setFilterChainProxySecurityConfigurer The webSecurityConfigurers parameter of this method is injected through @Value

@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}")

_getWebSecurityConfigurers() of AutowiredWebSecurityConfigurers IgnoreParents _Here is the configuration class to get all the WebSecurityConfigurer types _Normally we customize WebSecurityConfigurer by inheriting WebSecurityConfigurerAdapter

public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
	List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<>();
	Map<String, WebSecurityConfigurer> beansOfType = beanFactory
			.getBeansOfType(WebSecurityConfigurer.class);
	for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
		webSecurityConfigurers.add(entry.getValue());
	}
	return webSecurityConfigurers;
}

_Return to the setFilterChainProxySecurityConfigurer method with a code like this that loops through the apply method of webSecurity Configurer for all WebSecurityConfigurer types obtained above

for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
		webSecurity.apply(webSecurityConfigurer);
	}

_webSecurity integrates AbstractConfiguredSecurityBuilder, which provides the apply method and calls the add method internally

public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
	add(configurer);
	return configurer;
}

_add(configurer), the main thing is to save the WebSecurityConfigurer it passed into into LinkedHashMap configures, The main code is this.configurers.put(clazz, configs);

private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
	Assert.notNull(configurer, "configurer cannot be null");

	Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
			.getClass();
	synchronized (configurers) {
		if (buildState.isConfigured()) {
			throw new IllegalStateException("Cannot apply " + configurer
					+ " to already built object");
		}
		List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
				.get(clazz) : null;
		if (configs == null) {
			configs = new ArrayList<>(1);
		}
		configs.add(configurer);
		this.configurers.put(clazz, configs);
		if (buildState.isInitializing()) {
			this.configurersAddedInInitializing.add(configurer);
		}
	}
}

_The setFilterChainProxySecurityConfigurer method ends when all WebSecurityConfigurer type configurations are applied to WebSecurity

_Return to the springSecurityFilterChain() method of creating the filter chain

_It will determine if the webSecurityConfigurers we just created exist, create a new one if they don't exist, and then execute webSecurity.build() is important!

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
	boolean hasConfigurers = webSecurityConfigurers != null
			&& !webSecurityConfigurers.isEmpty();
	if (!hasConfigurers) {
		WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
				.postProcess(new WebSecurityConfigurerAdapter() {
				});
		webSecurity.apply(adapter);
	}
	return webSecurity.build();
}

_Ultimately there will be this code inside, focusing on the three methods init() configure() and performBuild()

@Override
protected final O doBuild() throws Exception {
	synchronized (configurers) {
		buildState = BuildState.INITIALIZING;

		beforeInit();
		init();

		buildState = BuildState.CONFIGURING;

		beforeConfigure();
		configure();

		buildState = BuildState.BUILDING;

		O result = performBuild();

		buildState = BuildState.BUILT;

		return result;
	}
}

_init() internal loop traverses all WebSecurityConfigurers, which executes to the WebSecurityConfigurerAdapter's

private void init() throws Exception {
	Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

	for (SecurityConfigurer<O, B> configurer : configurers) {
		configurer.init((B) this);
	}

	for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
		configurer.init((B) this);
	}
}

 configurer.init((B) this)

_It only needs to do two important things:

_Initialize the HttpSecurity object (note that it is not the same as WebSecurity); _Set the HttpSecurity object to be added to the securityFilterChainBuilders list of WebSecurity;

public void init(final WebSecurity web) throws Exception {
	final HttpSecurity http = getHttp();
	web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
		FilterSecurityInterceptor securityInterceptor = http
				.getSharedObject(FilterSecurityInterceptor.class);
		web.securityInterceptor(securityInterceptor);
	});
}

Initialize the HttpSecurity object in the getHttp() method:

protected final HttpSecurity getHttp() throws Exception {
	if (http != null) {
		return http;
	}

	DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
			.postProcess(new DefaultAuthenticationEventPublisher());
	localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);

	AuthenticationManager authenticationManager = authenticationManager();
	authenticationBuilder.parentAuthenticationManager(authenticationManager);
	authenticationBuilder.authenticationEventPublisher(eventPublisher);
	Map<Class<?>, Object> sharedObjects = createSharedObjects();

	http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
			sharedObjects);
	if (!disableDefaults) {
		// @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()
			.logout();
		// @formatter:on
		ClassLoader classLoader = this.context.getClassLoader();
		List<AbstractHttpConfigurer> defaultHttpConfigurers =
				SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

		for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
			http.apply(configurer);
		}
	}
	configure(http);
	return http;
}

_As you can see from the code, HttpSecurity is directly new born. Before creating HttpSecurity, the AuthenticationManagerBuilder object is initialized. Here is an important piece of code: AuthenticationManager authenticationManager = authenticationManager(); it creates an AuthenticationManager instance and opens the authenticationManager() method:

_The default implementation is in the WebSecurityConfigurerAdapter

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    this.disableLocalConfigureAuthenticationBldr = true;
}

_1. Configuration of Personalization Configuration Entry (AuthenticationManagerBuilder auth)

_We can personalize Authentication Manager Builder by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_Following is the first configuration entry to personalize by inheriting the WebSecurityConfigurerAdapter override configure(AuthenticationManagerBuilder auth)

/**
* @author johnny
* @create 2020-01-18 6:40 p.m.
**/
@Configuration
@Slf4j
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    super.configure(auth);
    log.info("[Test Customization Entry  configure(AuthenticationManagerBuilder auth)  Execution)");
}
}

_When an HttpSecurity instance is built, a default intercept configuration is added by default:

        http
            .csrf().and()
            .addFilter(new WebAsyncManagerIntegrationFilter())
            .exceptionHandling().and()
            .headers().and()
            .sessionManagement().and()
            .securityContext().and()
            .requestCache().and()
            .anonymous().and()
            .servletApi().and()
            .apply(new DefaultLoginPageConfigurer<>()).and()
            .logout();

_I'll pick a default method to expand on, for example, session management (), which internally creates a SessionManagementConfigurer and applies it

public SessionManagementConfigurer<HttpSecurity> sessionManagement() throws Exception {
	return getOrApply(new SessionManagementConfigurer<>());
}

_getOrApply has the most sentence code return apply(configurer);

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
		C configurer) throws Exception {
	C existingConfig = (C) getConfigurer(configurer.getClass());
	if (existingConfig != null) {
		return existingConfig;
	}
	return apply(configurer);
}

_apply(configurer) Note that here configurer passes in SessionManagementConfigurer

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
		throws Exception {
	configurer.addObjectPostProcessor(objectPostProcessor);
	configurer.setBuilder((B) this);
	add(configurer);
	return configurer;
}

_Finally, add(configurer) is called again; however, this is the initial configuration of the configurers for HttpSecurity, above which are the configurers for the configured WebSecurity. Don't confuse, these configurers will be created one by one as the corresponding Filter filters, which are described in detail later

private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
	Assert.notNull(configurer, "configurer cannot be null");

	Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
			.getClass();
	synchronized (configurers) {
		if (buildState.isConfigured()) {
			throw new IllegalStateException("Cannot apply " + configurer
					+ " to already built object");
		}
		List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
				.get(clazz) : null;
		if (configs == null) {
			configs = new ArrayList<>(1);
		}
		configs.add(configurer);
		this.configurers.put(clazz, configs);
		if (buildState.isInitializing()) {
			this.configurersAddedInInitializing.add(configurer);
		}
	}
}

_Image below: Many default configurations have been added for HttpSecurity

_Back to the getHttp() method

_Finally, configure(http) is called; again, this is a personalized configuration entry, and its default implementation is: provided by the WebSecurityConfigurerAdapter

_The default configuration is to intercept all requests that require authentication before they can be accessed. If no authentication is available, an authentication form is automatically generated that requires a user name and password to be entered.

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();
}

_2. Configuration for Personalized Configuration Entry (HttpSecurity http) _We can personalize HttpSecurity by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_OK, HttpSecurity has been initialized so far, and then you need to set the HttpSecurity object to add to the list of securityFilterChainBuilders for WebSecurity:

public void init(final WebSecurity web) throws Exception {
	final HttpSecurity http = getHttp();
	web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
		FilterSecurityInterceptor securityInterceptor = http
				.getSharedObject(FilterSecurityInterceptor.class);
		web.securityInterceptor(securityInterceptor);
	});
}

_When all the init methods of WebSecurityConfigurer are called, webSecurity.init() is finished

_webSecurity.configure() is then called, which is also implemented in AbstractConfiguredSecurityBuilder:

private void configure() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

    for (SecurityConfigurer<O, B> configurer : configurers) {
        configurer.configure((B) this);
    }
}

_Its main work is to iteratively call the configurer methods of all WebSecurityConfigurer s, the parameter is WebSeucrity itself, which is another important personalization entry:

_3. Configuration for Personalized Configuration Entry (WebSecurity web) _We can personalize WebSecurity by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_At this point, three important personalization entries have been called, which are often rewritten to implement the WebSecurityConfigurerAdapter:

1,configure(AuthenticationManagerBuilder auth);

2,configure(WebSecurity web);

3,configure(HttpSecurity http);

_Return to the webSecurity building process, followed by important calls:

O result = performBuild();

_PerfmBuild() is very important!!

@Override
protected Filter performBuild() throws Exception {
	Assert.state(
			!securityFilterChainBuilders.isEmpty(),
			() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
					+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
					+ "More advanced users can invoke "
					+ WebSecurity.class.getSimpleName()
					+ ".addSecurityFilterChainBuilder directly");
	int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
	List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
			chainSize);
	for (RequestMatcher ignoredRequest : ignoredRequests) {
		securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
	}
	for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
		securityFilterChains.add(securityFilterChainBuilder.build());
	}
	FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
	if (httpFirewall != null) {
		filterChainProxy.setFirewall(httpFirewall);
	}
	filterChainProxy.afterPropertiesSet();

	Filter result = filterChainProxy;
	if (debugEnabled) {
		logger.warn("\n\n"
				+ "********************************************************************\n"
				+ "**********        Security debugging is enabled.       *************\n"
				+ "**********    This may include sensitive information.  *************\n"
				+ "**********      Do not use in a production system!     *************\n"
				+ "********************************************************************\n\n");
		result = new DebugFilter(filterChainProxy);
	}
	postBuildAction.run();
	return result;


}

_First calculate the chainSize, which is ignoredRequests.size() + securityFilterChainBuilders.size();, if you do not configure ignoredRequests, it is securityFilterChainBuilders.size(), which is the number of HttpSecurities, essentially you configure several WebSecurityConfigurerAdapters, since each WebSecurityConfigurerAdapter corresponds to one HttpSecurity ConfigurerAdapterTy, and ignoredRequests is a request for FilterChainProxy, which is not available by default. If you need a strip to skip certain requests without authentication or authorization, you can configure it as follows:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/statics/**");
}

_In the above configuration, all requests starting with/statics will be ignored by FilterChainProxy.

_securityFilterChains.add(securityFilterChainBuilder.build()); This line is to initialize all filters, remember that there is a code above that sets HttpSecurity to the securityFilterChainBuilder of WebSecurity, which calls the HttpSecurity.build() method to initialize all HttpSecurity's filter chains

public void init(final WebSecurity web) throws Exception {
	final HttpSecurity http = getHttp();
	web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
		FilterSecurityInterceptor securityInterceptor = http
				.getSharedObject(FilterSecurityInterceptor.class);
		web.securityInterceptor(securityInterceptor);
	});
}

_Still come to the doBuild() method, but this time it's the HttpSecurity that executes the

@Override
protected final O doBuild() throws Exception {
	synchronized (configurers) {
		buildState = BuildState.INITIALIZING;

		beforeInit();
		init();

		buildState = BuildState.CONFIGURING;

		beforeConfigure();
		configure();

		buildState = BuildState.BUILDING;

		O result = performBuild();

		buildState = BuildState.BUILT;

		return result;
	}
}

_Focus on configure() This method calls the configure() of the corresponding filter configuration Create SessionManagementFilter internally and add it to HttpSecurity, that is, create the corresponding filters one by one with the configures of HttpSecurity

@Override
public void configure(H http) {
	SecurityContextRepository securityContextRepository = http
			.getSharedObject(SecurityContextRepository.class);
	SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(
			securityContextRepository, getSessionAuthenticationStrategy(http));
	if (this.sessionAuthenticationErrorUrl != null) {
		sessionManagementFilter.setAuthenticationFailureHandler(
				new SimpleUrlAuthenticationFailureHandler(
						this.sessionAuthenticationErrorUrl));
	}
	InvalidSessionStrategy strategy = getInvalidSessionStrategy();
	if (strategy != null) {
		sessionManagementFilter.setInvalidSessionStrategy(strategy);
	}
	AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
	if (failureHandler != null) {
		sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
	}
	AuthenticationTrustResolver trustResolver = http
			.getSharedObject(AuthenticationTrustResolver.class);
	if (trustResolver != null) {
		sessionManagementFilter.setTrustResolver(trustResolver);
	}
	sessionManagementFilter = postProcess(sessionManagementFilter);

	http.addFilter(sessionManagementFilter);
	if (isConcurrentSessionControlEnabled()) {
		ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);

		concurrentSessionFilter = postProcess(concurrentSessionFilter);
		http.addFilter(concurrentSessionFilter);
	}
}

_When configure() in doBuild(); when executed, the following HttpSecurity will see that all filters within it have been created

_Back to the doBuild() method, there is performBuild() calling performBuild() of HttpSecurity. By default, all the above filters are sorted first, using FilterComparator() to sort them. This is not expanded here, it is the order above on the graph where the article begins.

@Override
protected DefaultSecurityFilterChain performBuild() {
	filters.sort(comparator);
	return new DefaultSecurityFilterChain(requestMatcher, filters);
}

_Finally, the default implementation of SecurityFilterChain, DefaultSecurityFilterChain, is returned.

_Create the most important instance of FilterChainProxy after all SecurityFilterChain s have been built.

FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

_Up to this point, Spring Security initialization has been completed, including springSecurityFilterChain initialization. We achieve personalization by inheriting WebSecurityConfigurerAdapter. Three important personalization entries are mentioned, and WebSecurityConfigurerAdapter can be configured with more than one. The corresponding interface is that there will be multiple instances of SecurityFilterChain, butIt is that they are still in the same FilterChainProxy, matched by RequestMatcher and passed into the corresponding SecurityFilterChain to execute the request.

_5. Personalized Entry Configuration (Extended WebSecurityConfigurerAdapter)

_Important personalization entries are where they are invoked and have been explained in the springSecurityFilterChain source code initialized above, here is a summary of the knowledge

_1. Configuration of Personalization Configuration Entry (AuthenticationManagerBuilder auth)

_We can personalize Authentication Manager Builder by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_2. Configuration for Personalized Configuration Entry (HttpSecurity http) _We can personalize HttpSecurity by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_3. Configuration for Personalized Configuration Entry (WebSecurity web) _We can personalize WebSecurity by inheriting the WebSecurityConfigurerAdapter and overriding this method.

_Implementing the WebSecurityConfigurerAdapter often requires overrides:

1,configure(AuthenticationManagerBuilder auth);

2,configure(WebSecurity web);

3,configure(HttpSecurity http);

_6. Summary

_This article mainly explains _1.SpringBoot's support class SecurityAutoConfiguration for automatic configuration of SpringSecurity, _2. Core Note@EnableWebSecurity _3. Source code for the initialization process of springSecurityFilterChain, the core filter chain of SpringSecurity Source part or calm down to see more fuel!

Personal blogging system: https://www.askajohnny.com Welcome! This article is published by blog OpenWrite Release!

Keywords: Programming Spring Session SpringBoot Java

Added by ryansmith44 on Sun, 19 Jan 2020 06:46:31 +0200