Use some of the ideas spring security of (a)

Foreword

The company recently made a new project, using the spring security framework for authentication and authorization. It took three days to complete:

  • Account password
  • jwt
  • url-level access

In fact, before a project it is also used, but basically ctrl + c, ctrl + v. Not too understand the overall framework of this process, it is foggy, but actually ran quite normal.

I have seen some online tutorials before, but did not look too understand, because they though about the most important things are talked about, and some less important, but it can nexus point not mentioned, so see or relatively ignorant, typically I understand, but I want to write, I will not! So in Sidongfeidong state, he embarked on this road of no return.

Ado, get to the point! ! !

Pre-knowledge

spring security is achieved through a series of function corresponding to a filter, each filter corresponds with one function. So in this case the order of the filter is very important, because each filter are likely to modify the original data, or only through a filter to filter the request to the next filter. This is the spring of the core principles of security. Ye looks like pretty simple.

Filter chain

This is an entire filter chain I used in the project, where 6,7 is the custom.

6: is a filter password account, 7: is a filter for verification jwt, 12: check permission for a filter

Account password

Account password is almost all projects would have for a function.

Filter Code

public class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    CustomUsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        String body = StreamUtils.copyToString(httpServletRequest.getInputStream(), Charset.forName("UTF-8"));
        String username = null;
        String password = null;
        if (StringUtils.hasText(body)){
            JSONObject jsonObject = JSON.parseObject(body);
            username = jsonObject.getString("username");
            password = jsonObject.getString("password");
        }

        if (username == null){
            username = "";
        }
        if (password == null){
            password = "";
        }
        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

复制代码

Let's analyze the code:

First, we inherit an abstract class filter, the filter class is actually an ordinary filter, but he did some things in it, and we do not need to know these things specifically used to doing, as long as we it contains a known doFiltermethod

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;

	if (!requiresAuthentication(request, response)) {
		chain.doFilter(request, response);

		return;
	}

	if (logger.isDebugEnabled()) {
		logger.debug("Request is to process authentication");
	}

	Authentication authResult;

	try {
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			// return immediately as subclass has indicated that it hasn't completed
			// authentication
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}
	catch (InternalAuthenticationServiceException failed) {
		logger.error(
				"An internal error occurred while trying to authenticate the user.",
				failed);
		unsuccessfulAuthentication(request, response, failed);

		return;
	}
	catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);

		return;
	}

	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}

    successfulAuthentication(request, response, chain, authResult);
}
复制代码

We can see where the implementation of attemptAuthenticationthis method, the name of this method is not the way we like from the filter definitions rewrite the name of it, yes, exactly, and it is our custom filters here actually calls inside overridden attemptAuthenticationmethod

I explain here is actually used a template pattern, template mode is actually most common features have been implemented by the parent, while some special features can be delayed to subclasses to achieve. But the process is still calling the parent to decide, but the specific sub-class implements a single function processes only. In java, there are many places to use this design pattern, such as the famous abstract concurrency control for synchronous queue AbstractQueuedSynchronized , is to use a template model, we are interested can look at the source code, written especially subtle.

I haircut whole process: First, the filter chain will perform to CustomUsernamePasswordAuthenticationFilterthe parent of the filter doFiltermethod, and doFilterthe method will be executed overridden attemptAuthenticationmethod.

Then we enter into the attemptAuthenticationprocess, look at this method are done:

First, acquired bodyin the username and password, and a configuration UsernamePasswordAuthenticationTokenobject to the next method.

Focus here !!!

It constructed object passed on, then it is passed to who?

First, let's look at getAuthenticationManager()this method is doing

protected AuthenticationManager getAuthenticationManager() {
	return authenticationManager;
}
复制代码

Actually returned a certified manager, but the manager of this certification, what is?

Then we enter into the AuthenticationManagerinside, which is found in an interface only. Since it is an interface, it certainly can not be instantiated, but here it instantiates an AuthenticationManagerobject, the truth is only one, spring security provides a default class that implements the interface. Yes, the fact is that this is ProviderManagerthe class.

We first look at his source

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
	// ~ Static fields/initializers
	// =====================================================================================

	private static final Log logger = LogFactory.getLog(ProviderManager.class);

	// ~ Instance fields
	// ================================================================================================

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
	private List<AuthenticationProvider> providers = Collections.emptyList();
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private AuthenticationManager parent;
	private boolean eraseCredentialsAfterAuthentication = true;

	public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

	public ProviderManager(List<AuthenticationProvider> providers,
			AuthenticationManager parent) {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}
}
复制代码

I put some irrelevant comments and code are removed, leaving only the useful part, or too long, not too good-looking.

First, we found that there is a authenticatemethod, the name of this method is not with us CustomUsernamePasswordAuthenticationFilterlike the last line of code inside the same, yes, that is to call this method here.

This method is mainly used to manage Provider, in fact, is called here the various Providermethods used to verify that all of Provider are realized AuthenticationProviderthis interface.

We mainly see is provider.authenticate(authentication)this statement, he is really used method validation. The real validation process is to achieve by our own, or use the default.

And we realize account password to be used Provideris already provided by the spring security to us DaoAuthenticationProvider, then here it is a template mode (this mode really quite common ah)!

Together we analyze this class in the end to do what?

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	/**
	 * The plaintext password used to perform
	 * PasswordEncoder#matches(CharSequence, String)}  on when the user is
	 * not found to avoid SEC-2056.
	 */
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

	// ~ Instance fields
	// ================================================================================================

	private PasswordEncoder passwordEncoder;

	/**
	 * The password used to perform
	 * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
	 * not found to avoid SEC-2056. This is necessary, because some
	 * {@link PasswordEncoder} implementations will short circuit if the password is not
	 * in a valid format.
	 */
	private volatile String userNotFoundEncodedPassword;

	private UserDetailsService userDetailsService;

	private UserDetailsPasswordService userDetailsPasswordService;

	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}

	// ~ Methods
	// ========================================================================================================

	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsPasswordService(
			UserDetailsPasswordService userDetailsPasswordService) {
		this.userDetailsPasswordService = userDetailsPasswordService;
	}
}

复制代码

AbstractUserDetailsAuthenticationProviderThis class is truly AuthenticationProviderthe abstract class interface.

The abstract class is a template pattern in the parent class is a subclassDaoAuthenticationProvider

Abstract class just defines the specific process, the real work is to verify the implementation by subclasses to concrete implementation.

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
复制代码

The more important is the retrieveUsermethod that is user access to information systems, and here is the account password, and then put them into a constructed UserDetailsobject.

The calibration method is additionalAuthenticationChecksthat the method accepts two parameters, one is UsernamePasswordAuthenticationToken, the other is UserDetailsto explain before through knowledge we know, UsernamePasswordAuthenticationTokenis the object of the request by the user login account, password structure. UserDetailsIs the object of accounts, passwords construction of the system. As long as we compare these two objects are the same information, we can know whether the user can log on.

After successful validation will call a createSuccessAuthenticationmethod that calls a callback successful, then we can return token to the user in the callback.

If the validation fails, also called a failed method, the principle is the same with success, but returns information to the user's information is wrong.

The success and failure callback we also need to customize. Success is achieved through AuthenticationSuccessHandlerthe interface, failure is by implementing the AuthenticationFailureHandlerinterface.

We know that retrieveUserthe method will obtain user information in the system, then it is how to obtain it?

He will execute this statementthis.getUserDetailsService().loadUserByUsername(username);

getUserDetailsService()This method with the Service on our business is the same, but spring security requires us to realize that they provide UserDetailsServicean interface only one method loadUserByUsernamethat accepts a usernameparameter, we are obtaining information about the user in the process. Because DAO, so I came to get the user information by querying the database username. Then a configured UserDetailsobject is returned.

Of course, this UserDetailsis also an interface, we need to implement this interface, a custom UserDetailsimplementation class to use.

Finally, we just pass a Configurer object all the custom components combine it.

package com.liangxin.airport.security;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;

/**
 * 登录校验的配置,就是将各个有关的类进行组合,让spring security可以识别的出来
 *
 * @author LHD
 * @date 2019/12/9 17:17
 */
public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

    private CustomUsernamePasswordAuthenticationFilter authFilter;

    public JsonLoginConfigurer(){
        this.authFilter = new CustomUsernamePasswordAuthenticationFilter();
    }

    @Override
    public void configure(B http){
        // 设置filter使用AuthenticationManager
        authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 设置失败的handler
        authFilter.setAuthenticationFailureHandler(new JsonLoginFailureHandler());
        // 不将认证后的context放入session
        authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());

        CustomUsernamePasswordAuthenticationFilter filter = postProcess(authFilter);

        http.addFilterAfter(filter, LogoutFilter.class);
    }

    public JsonLoginConfigurer<T, B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
        authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        return this;
    }
}

复制代码

to sum up

Because the source code used in a lot of interfaces, inheritance, design patterns, so looks like it would be more around.

I'm here to summarize the whole process:

Filter -> ProviderManager -> AbstractUserDetailsAuthenticationProvider -> DaoAuthenticationProvider -> AuthenticationSuccessHandler

Filter configured first enters a token information provided by the user (for later verify), then ProviderManagerproceeds to the corresponding Provider, then Provideracquired by the username information to the user in the system, then the user with the system token before the compare information on the call is successful AuthenticationSuccessHandler, it fails to call AuthenticationFailureHandler.

end

Here we put the entire login process finished, because the company's projects, there is a confidentiality agreement, all of the source code can not be directly attached to it, but if you still do not know where you can leave a message at any time, I will try to answer.

Guess you like

Origin juejin.im/post/5df0c06ff265da3397729c82