Preface
The popular general authorization frameworks now include the shiro of apache and the Spring Family Spring Security. When it comes to today's microservice authentication, we need to use our authorization framework to build our own authentication services. Today, the Prime Minister has been the Prime Minister.
Spring Security mainly implements Authentication (authentication, solution who are you?) and Access Control (access control, that is, what are you allowed to do?, also known as Authorization). Spring Security separates authentication from authorization architecture and provides extension points.
Core Objects
The main code is under the spring-security-core package. To understand Spring Security, you need to pay attention to the core objects inside.
SecurityContextHolder, SecurityContext and Authentication
SecurityContextHolder is a storage container for SecurityContext. ThreadLocal storage is used by default, which means that SecurityContext methods are available in the same thread.
SecurityContext mainly stores the application's principal information and is represented by Authentication in Spring Security.
Get principal:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (principal instanceof UserDetails) {String username = ((UserDetails)principal).getUsername();} else {String username = principal.toString();}In Spring Security, you can take a look at the Authentication definition:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); /** * Usually password*/ Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP * address, certificate serial number etc. */ Object getDetails(); /** * Used to identify whether it is authenticated. If you log in with a username and password, it is usually the username*/ Object getPrincipal(); /** * Whether it is authenticated*/ boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}In practical applications, UsernamePasswordAuthenticationToken is usually used:
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { }public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {}A common authentication process is usually like this: Create a UsernamePasswordAuthenticationToken and then hand it over to the authenticationManager for authentication (described in detail later). If the authentication is passed, the Authentication information will be stored through the SecurityContextHolder.
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());Authentication authentication = this.authenticationManager.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails and UserDetailsService
UserDetails is a key interface in Spring Security, which is used to represent a principal.
public interface UserDetails extends Serializable { /** * User authorization information can be understood as role*/ Collection<? extends GrantedAuthority> getAuthorities(); /** * User password* * @return the password */ String getPassword(); /** * Username* */ String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();}UserDetails provides the necessary information required for authentication. In actual use, you can implement UserDetails by yourself and add additional information, such as email, mobile and other information.
In Authentication, principal is usually the username. We can get UserDetails through principal through UserDetailsService:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}GrantedAuthority
As mentioned in UserDetails, GrantedAuthority can be understood as a role, such as ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR.
summary
Authentication certification
AuthenticationManager
The authentication is mainly achieved through the AuthenticationManager interface, which only contains one method:
public interface AuthenticationManager { Authentication authentication(Authentication authentication) throws AuthenticationException;}The authenticate() method mainly does three things:
AuthenticationException is a runtime exception, which is usually handled by the application in a common way. User code usually does not need to be caught and processed specifically.
The default implementation of AuthenticationManager is ProviderManager, which delegates a set of AuthenticationProvider instances to implement authentication.
AuthenticationProvider and AuthenticationManager are similar to Authentication, both contain authentication, but it has an additional method support to allow querying whether the caller supports a given Authentication type:
public interface AuthenticationProvider { Authentication authentication(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication);}ProviderManager contains a set of AuthenticationProviders. When executing authentication, it traverses Providers and then calls support. If supported, it executes the authenticate method that traverses the current provider. If a provider is authenticated successfully, break.
public Authentication authentication(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = 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 e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = 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 = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } 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}")); } prepareException(lastException, authentication); throw lastException; }As can be seen from the above code, ProviderManager has an optional parent. If the parent is not empty, parent.authenticate(authentication) is called
AuthenticationProvider
AuthenticationProvider has many implementations. The one you are most concerned about is usually DaoAuthenticationProvider, inherited from AbstractUserDetailsAuthenticationProvider. The core is to implement authentication through UserDetails. DaoAuthenticationProvider will be automatically loaded by default and does not need to be manually configured.
Let’s first look at the AbstractUserDetailsAuthenticationProvider and look at the most core authentication:
public Authentication authentication(Authentication authentication) throws AuthenticationException { // Must be UsernamePasswordAuthenticationToken Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Get username String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // Get UserDetails from cache user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // retrieveUser abstract method to get user 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 { // Pre-check, DefaultPreAuthenticationChecks, check whether the user is locked or whether the account is available for preAuthenticationChecks.check(user); // Abstract method, custom check additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (ie not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } // Post-check DefaultPostAuthenticationChecks, check isCredentialsNonExpired postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }The above test is mainly based on UserDetails implementation, where the user acquisition and verification logic are implemented by specific classes. The default implementation is DaoAuthenticationProvider. The core of this class is to allow developers to provide UserDetailsService to obtain UserDetails and PasswordEncoder to verify whether the password is valid:
private UserDetailsService userDetailsService; private PasswordEncoder passwordEncoder;
To see the specific implementation, retrieveUser, directly call userDetailsService to get the user:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException( repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }Let's look at the verification:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } // Get the user password String presentedPassword = authentication.getCredentials().toString(); // Compare whether the password after passwordEncoder is the same as the password of userdetails if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }Summary: To customize authentication, use DaoAuthenticationProvider, you only need to provide it with PasswordEncoder and UserDetailsService.
Customize Authentication Managers
Spring Security provides a Builder class AuthenticationManagerBuilder, which allows you to quickly implement custom authentication.
See the official source code description:
SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider's.
AuthenticationManagerBuilder can be used to build an AuthenticationManager, which can create memory-based authentication, LDAP authentication, JDBC authentication, and add UserDetailsService and AuthenticationProvider.
Simple use:
@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class ApplicationSecurity extends WebSecurityConfigurerAdapter { public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; this.corsFilter = corsFilter; this.problemSupport = problemSupport; } @PostConstruct public void init() { try { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } catch (Exception e) { throw new BeanInitializationException("Security configuration failed", e); } } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport) .and() .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/register").permitAll() .antMatchers("/api/activate").permitAll() .antMatchers("/api/authenticate").permitAll() .antMatchers("/api/account/reset-password/init").permitAll() .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/profile-info").permitAll() .antMatchers("/api/**").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/v2/api-docs/**").permitAll() .antMatchers("/swagger-resources/configuration/ui").permitAll() .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN) .and() .apply(securityConfigurerAdapter()); }}Authorization and Access Control
Once the authentication is successful, we can continue to authorize, which is implemented through AccessDecisionManager. There are three implementations of the framework, the default is AffirmativeBased, which is made through AccessDecisionVoter, which is a bit like the ProviderManager is entrusted to AuthenticationProviders for authentication.
public void decision(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; // traverse DecisionVoter for (AccessDecisionVoter voter : getDecisionVoters()) { // Voting int result = vote.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } // veto if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }Let’s take a look at AccessDecisionVoter:
boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
object is the resource that the user wants to access, and ConfigAttribute is the condition that the object needs to be met. Usually payload is a string, such as ROLE_ADMIN. So let's take a look at the implementation of RoleVoter. The core is to extract the GrantedAuthority from the authentication, and then compare with ConfigAttribute whether the conditions are met.
public boolean supports(ConfigAttribute attribute) { if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) { return true; } else { return false; } } public boolean supports(Class<?> clazz) { return true; } public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if(authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; // Get GrantedAuthority informationCollection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { // Access denied by default result = ACCESS_DENIED; // Attempt to find a matching granted authority for (GrantedAuthority authority : authorities) { // Determine whether there is a matching authority if (attribute.getAttribute().equals(authority.getAuthority())) { // You can access return ACCESS_GRANTED; } } } } return result; }Here I have to ask, where did ConfigAttribute come from? In fact, it is in the configuration of ApplicationSecurity above.
How to implement web security
Spring Security (for UI and HTTP backends) in the web layer is based on Servlet Filters, and the following figure shows typical hierarchy of handlers for a single HTTP request.
Spring Security is registered to the web layer through FilterChainProxy as a single Filter, Filter inside Proxy.
FilterChainProxy is equivalent to a filter container. Through VirtualFilterChain, each internal filter is called in sequence.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<Filter> additionalFilters; private final FirewalledRequest firewalledRequest; private final int size; private int currentPosition = 0; private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; this.size = additionalFilters.size(); this.firewalledRequest = firewalledRequest; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } // Deactivate path striping as we exit the security filter chain this.firewalledRequest.reset(); originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of " + size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } nextFilter.doFilter(request, response, this); } } } }refer to
https://spring.io/guides/topicals/spring-security-architecture/
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#overall-architecture
Summarize
The above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.