Security, oauth2.0 related overview

security

The so-called global context implementation of spring security

Implemented using ThreadLocal

Spring security related components

AuthenticationManagerBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("admin").password("admin").roles("USER");
    }
}

To perform authentication-related configuration in WebSecurityConfigurerAdapter, you can use configure(AuthenticationManagerBuilder auth) to expose an AuthenticationManager builder: AuthenticationManagerBuilder. As shown above, we have completed the configuration of the user in memory.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

If your application has only one WebSecurityConfigurerAdapter, then the gap between them can be ignored, and the difference between the two can be seen from the method name: AuthenticationManagerBuilder injected with @Autowired is a global authenticator, and the scope can span multiple WebSecurityConfigurerAdapter , and affect the method-based security control; and the protected configure() method is similar to an anonymous inner class, whose scope is limited to a WebSecurityConfigurerAdapter. In the official documentation, scenarios and demos for configuring multiple WebSecurityConfigurerAdapters are also given.

SecurityContextHolder

SecurityContextHolder is used to store security context (security context) information. Who is the currently operating user, whether the user has been authenticated, what role permissions does he have...these are all stored in the SecurityContextHolder. SecurityContextHolder uses ThreadLocal strategy to store authentication information by default. Seeing ThreadLocal also means that this is a thread-bound strategy. Spring Security automatically binds the authentication information to the current thread when the user logs in, and automatically clears the authentication information of the current thread when the user logs out. But the premise of all this is that you use Spring Security in the web scene, and if it is a Swing interface, Spring also provides support, and the SecurityContextHolder strategy needs to be replaced.

Get the current user's information Because the identity information is bound to the thread, you can use the static method to get the user information anywhere in the program. A typical example of getting the name of the currently logged in user is as follows:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) { 
    String username = ((UserDetails)principal).getUsername(); 
} else { 
    String username = principal.toString(); 
}

getAuthentication() returns authentication information, and getPrincipal() returns identity information again. UserDetails is an interface that Spring encapsulates identity information.

Authentication

package org.springframework.security.core;// <1>
//Authentication是spring security包中的接口
//,直接继承自Principal类,而Principal是位于java.security包中的。
//可以见得,Authentication在spring security中是最高级别的身份/认证的抽象
public interface Authentication extends Principal, Serializable { // <1>
    Collection<? extends GrantedAuthority> getAuthorities(); // <2>

    Object getCredentials();// <2>

    Object getDetails();// <2>

    Object getPrincipal();// <2>

    boolean isAuthenticated();// <2>

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

getAuthorities(), a list of authority information, defaults to some implementation classes of the GrantedAuthority interface, usually a series of strings representing authority information.

getCredentials(), password information, the password string entered by the user, is usually removed after authentication to ensure security.

getDetails(), detailed information, the implementation interface in web applications is usually WebAuthenticationDetails, which records the visitor's ip address and sessionId value.

getPrincipal(), the most important identity information, returns the implementation class of the UserDetails interface in most cases, and is also one of the commonly used interfaces in the framework

AuthenticationManager

Friends who are new to Spring Security believe that they will be confused by AuthenticationManager, ProviderManager, AuthenticationProvider... so many similar Spring authentication classes, but as long as they sort out a little, they can understand their connection and the intention of the designer. AuthenticationManager (interface) is the core interface related to authentication, and it is also the starting point for initiating authentication, because in actual needs, we may allow users to log in with username + password, and at the same time allow users to log in with email address + password, mobile phone number + password, or even , may allow users to log in with fingerprints (there is such an operation? Didn’t expect it), so AuthenticationManager generally does not directly authenticate. The commonly used implementation class ProviderManager of the AuthenticationManager interface will maintain a List list internally to store multiple authentication methods. In fact, this It is the application of the delegator pattern (Delegate). That is to say, there is always only one core authentication entry: AuthenticationManager, and different authentication methods: username + password (UsernamePasswordAuthenticationToken), email + password, and mobile phone number + password login correspond to three AuthenticationProviders. In this way, it will be easier to understand the four and four? Friends who are familiar with shiro can understand AuthenticationProvider as Realm. Under the default policy, only one AuthenticationProvider authentication is required to be considered a successful login.

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {

    // 维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
          
    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                   //移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
             //发布登录成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
       }
       ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

The List in the ProviderManager will be authenticated in order. If the authentication is successful, it will return immediately. If the authentication fails, it will return null. The next AuthenticationProvider will continue to try to authenticate. If all authenticators fail to authenticate successfully, the ProviderManager will throw a ProviderNotFoundException.

UserDetails与UserDetailsService

The UserDetails interface represents the most detailed user information. This interface covers some necessary user information fields, and the specific implementation class extends it

public interface UserDetails extends Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();

   String getPassword();

   String getUsername();

   boolean isAccountNonExpired();

   boolean isAccountNonLocked();

   boolean isCredentialsNonExpired();

   boolean isEnabled();
}

It is very similar to the Authentication interface. For example, they both have username and authorities. Distinguishing them is also one of the key points of this article. Authentication's getCredentials() and UserDetails' getPassword() need to be treated differently. The former is the password credential submitted by the user, while the latter is the user's correct password. The authenticator is actually a comparison between the two. The getAuthorities() in Authentication is actually formed by passing the getAuthorities() of UserDetails. Remember the getUserDetails() method in the Authentication interface? The UserDetails user details are filled after passing the AuthenticationProvider.

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

The responsibilities of UserDetailsService and AuthenticationProvider are often confused by people, and questions about them are common in the FAQ and issues of the documentation. Just remember a little, knock on the blackboard! ! ! UserDetailsService is only responsible for loading user information from a specific place (usually a database), nothing more, remembering this, you can avoid many detours. Common implementation classes of UserDetailsService include JdbcDaoImpl and InMemoryUserDetailsManager. The former loads users from the database, and the latter loads users from memory. You can also implement UserDetailsService yourself, which is usually more flexible.

DaoAuthenticationProvider

One of the most commonly used implementations of AuthenticationProvider is DaoAuthenticationProvider. As the name suggests, Dao is the abbreviation of Data Access Layer, which also implies the implementation idea of ​​this identity authenticator.

According to our most intuitive thinking, how to authenticate a user? The user submits the user name and password at the front desk, and the user name and password are saved in the database. Authentication is responsible for comparing the same user name, whether the submitted password is the same as the saved password. In Spring Security. The submitted username and password are encapsulated into UsernamePasswordAuthenticationToken, and the task of loading users according to the username is handed over to UserDetailsService. In DaoAuthenticationProvider, the corresponding method is retrieveUser. Although there are two parameters, retrieveUser only has the first one. The parameter plays the main role and returns a UserDetails. It is also necessary to complete the comparison between the UsernamePasswordAuthenticationToken and the UserDetails password. This is done by the additionalAuthenticationChecks method. If the void method does not throw an exception, the comparison is considered successful. The process of comparing passwords uses PasswordEncoder and SaltSource. The concepts of password encryption and salt do not need to be repeated. They are designed to ensure security and are relatively basic concepts.

If you have been confused by these concepts, you may wish to understand DaoAuthenticationProvider in this way: it obtains the user name and password submitted by the user, compares their correctness, and if correct, returns the user information in a database (assuming that the user information is stored in the database ).

spring security interception chain

  • SecurityContextPersistenceFilter

  • is the first filter executed and serves two purposes:

  • 1. Determine whether the sission used already exists in the SecurityContext. If it exists, take it out and put it in the SecurityContextHolder in the Security context for use by other parts. If it does not exist, create one and store it in the SecurityContextHolder.

  • 2. After all the filters are executed, clear the SecurityContextHolder, because the SecurityContextHolder is based on ThreadLocal , if the ThreadLocal is not cleared at the end, it will be affected by the server mechanism

  • LogoutFilter

  • Only handle logout requests. When a logout request occurs, destroy the logout user's session, clear the SecurityContextHolder, and then redirect to the logout success page. It can be combined with the shutdown function to clear the user's cooike when it is closed

  • AbstractAuthenticationProcessingFilter

  • Filter for processing form login. All operations related to form login are performed here. When logging in, it is judged whether the username and password are valid, and if valid, it will jump to the success page

  • DefaultLoginPageGeneratingFilter

  • Used to generate a default login page, although it has some functions, it is too ugly to use in actual projects

  • BasicAuthenticationFilter

  • Used for Basic authentication, similar to AbstractAuthenticationProcessingFilter but with different authentication methods

  • SecurityAuthenticationFilter

  • It is used to wrap the customer's request, and the purpose is to provide additional data to the follow-up program on the basis of the original request, such as directly providing the currently logged-in user name when removing user

  • RememberMeAuthenticationFilter

  • Realize the RememberMe function. When there is a RememberMe tag in the user cookie, the SecurityContext will be automatically created according to the tag and the corresponding permissions will be granted. RememberMe in spring Security relies on cookie implementation. When the user chooses to use RememberMe when logging in, it will be generated for the user after the user logs in. A unique identifier, and save the identifier in the cookie

  • AnonymousAuthenticationFilter

  • It is used to ensure that users are assigned anonymous user permissions when they are not logged in. Of course, many projects will also disable anonymous users.

  • ExceptionTeanslationFilter

  • In order to handle the exception thrown by filterSecurityException, the request is redirected to the corresponding page, or the corresponding response error code is returned

  • SessionManagementFilter

  • As long as it is to defend against session forgery attacks, as long as the current user's current session is destroyed after successful login, and a new session is generated

  • FilterSecurityInterceptor

  • The user's permission control is included in this filter, the function is:

  • 1. If the user has not logged in, an exception that has not been logged in is thrown

  • 2. If the user has logged in but does not have permission to access the current resource, an exception of access denied will be thrown

  • 3. If the user has logged in and has the permission to access the current resource, it will be released

  • How are these eleven interceptors executed in order?

  • FilterChainProxy: This class will call a set of filters in order to use their corresponding work, and to achieve springioc to get other dependent resources

How does Spring Security complete authentication?

1 The username and password are obtained by the filter and encapsulated into Authentication, usually the implementation class UsernamePasswordAuthenticationToken.

2 AuthenticationManager Identity Manager is responsible for verifying this Authentication

3 After the authentication is successful, the AuthenticationManager identity manager returns an Authentication instance filled with information (including the above-mentioned permission information, identity information, and detailed information, but the password is usually removed).

4 The SecurityContextHolder security context container sets the Authentication filled with information in Step 3 into it through the SecurityContextHolder.getContext().setAuthentication(…) method.

This is an abstract authentication process, and in the whole process, if you don't get entangled in the details, there is actually only one AuthenticationManager that we haven't touched. We will introduce this identity manager in the next section. Converting the above process into code is the following process:

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
    System.out.println("Please enter your username:");
    String name = in.readLine();
    System.out.println("Please enter your password:");
    String password = in.readLine();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
    } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
    }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
            SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
    }
}

Briefly describe the spring security core filter

SecurityContextPersistenceFilter has two main responsibilities: when a request comes, create SecurityContext security context information, and clear SecurityContextHolder when the request ends.

HeaderWriterFilter (not introduced in the document, non-core filter) is used to add some headers to the http response, such as X-Frame-Options, X-XSS-Protection*, X-Content-Type-Options.

CsrfFilter is a filter that is enabled by default in the spring4 version to prevent csrf attacks. People who understand the separation of front and back ends will not be unfamiliar with this attack method. A problem that needs to be paid attention to when using json interaction between front and back ends.

LogoutFilter As the name suggests, a filter that handles logout

UsernamePasswordAuthenticationFilter This will focus on the analysis. The username and password submitted in the form are encapsulated into a token for a series of authentications. This is mainly done through this filter. In the form authentication method, this is the most critical filter.

RequestCacheAwareFilter (not introduced in the document, non-core filter) internally maintains a RequestCache for caching request requests

SecurityContextHolderAwareRequestFilter This filter wraps the ServletRequest once, making the request have a richer API

AnonymousAuthenticationFilter Anonymous identity filter, this filter is very important personally, it needs to be put together with UsernamePasswordAuthenticationFilter to understand, spring security has also gone through a set of authentication processes in order to be compatible with unlogged access, but it is just an anonymous identity.

SessionManagementFilter and session-related filters maintain a SessionAuthenticationStrategy internally, which are used in combination to prevent session-fixation protection attacks and limit the number of multiple sessions opened by the same user

ExceptionTranslationFilter is directly translated into an exception translation filter, which is quite visual. This filter does not handle exceptions itself, but hands over the exceptions that occur during the authentication process to some classes maintained internally for processing. Specifically, those classes will be introduced in detail later. Find the corresponding link at the top or in the column of the home page.

FilterSecurityInterceptor This filter determines the permissions to access a specific path, the role of the user to access, and what are the permissions? What roles and permissions are required for the access path? These judgments and processing are performed by this class.

Can you talk about the principle of permitAll in the spring security configuration class?

permitAll can directly ignore the corresponding conditions. In spring security, there will be a super administrator user who grants permitAll. This super administrator user has super permissions and can pass through all filter chains.

Introduce SecurityContextPersistenceFilter

Just imagine, if we don't use Spring Security, if we save user information, we will consider using Session in most cases, right? The same is true in Spring Security. After a user logs in once, subsequent visits are identified by sessionId, so that the user is considered to have been authenticated. SecurityContextHolder stores user information, and how authentication-related information is stored in it is through SecurityContextPersistenceFilter. The two main functions of SecurityContextPersistenceFilter are to create SecurityContext security context information when a request comes, and to clear SecurityContextHolder when the request ends. One of the design concepts of microservices needs to achieve stateless service communication, and stateless in the http protocol means that no session is allowed, which can be achieved through setAllowSessionCreation(false), which does not mean that SecurityContextPersistenceFilter becomes useless, because it also It is responsible for clearing user information. In Spring Security, although the security context information is stored in the Session, we should not directly manipulate the Session in actual use, but use the SecurityContextHolder.

org.springframework.security.web.context.SecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean {

   static final String FILTER_APPLIED = "__spring_security_scpf_applied";
   //安全上下文存储的仓库
   private SecurityContextRepository repo;
  
   public SecurityContextPersistenceFilter() {
      //HttpSessionSecurityContextRepository是SecurityContextRepository接口的一个实现类
      //使用HttpSession来存储SecurityContext
      this(new HttpSessionSecurityContextRepository());
   }

   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
         throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;

      if (request.getAttribute(FILTER_APPLIED) != null) {
         // ensure that filter is only applied once per request
         chain.doFilter(request, response);
         return;
      }
      request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
      //包装request,response
      HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
            response);
      //从Session中获取安全上下文信息
      SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
      try {
         //请求开始时,设置安全上下文信息,这样就避免了用户直接从Session中获取安全上下文信息
         SecurityContextHolder.setContext(contextBeforeChainExecution);
         chain.doFilter(holder.getRequest(), holder.getResponse());
      }
      finally {
         //请求结束后,清空安全上下文信息
         SecurityContext contextAfterChainExecution = SecurityContextHolder
               .getContext();
         SecurityContextHolder.clearContext();
         repo.saveContext(contextAfterChainExecution, holder.getRequest(),
               holder.getResponse());
         request.removeAttribute(FILTER_APPLIED);
         if (debug) {
            logger.debug("SecurityContextHolder now cleared, as request processing completed");
         }
      }
   }

}

Filters are generally responsible for the core processing flow, and the specific business implementation is usually handed over to other entity classes aggregated in it. This is very common in the design of Filter, and it also conforms to the separation of duties model. For example, the work of storing security context and reading security context is completely entrusted to HttpSessionSecurityContextRepository, and there are several methods in this class that can be explained a little bit, so that we can understand the internal workflow

org.springframework.security.web.context.HttpSessionSecurityContextRepository

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
   // 'SPRING_SECURITY_CONTEXT'是安全上下文默认存储在Session中的键值
   public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
   ...
   private final Object contextObject = SecurityContextHolder.createEmptyContext();
   private boolean allowSessionCreation = true;
   private boolean disableUrlRewriting = false;
   private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;

   private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

   //从当前request中取出安全上下文,如果session为空,则会返回一个新的安全上下文
   public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
      HttpServletRequest request = requestResponseHolder.getRequest();
      HttpServletResponse response = requestResponseHolder.getResponse();
      HttpSession httpSession = request.getSession(false);
      SecurityContext context = readSecurityContextFromSession(httpSession);
      if (context == null) {
         context = generateNewContext();
      }
      ...
      return context;
   }

   ...

   public boolean containsContext(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      if (session == null) {
         return false;
      }
      return session.getAttribute(springSecurityContextKey) != null;
   }

   private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
      if (httpSession == null) {
         return null;
      }
      ...
      // Session存在的情况下,尝试获取其中的SecurityContext
      Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
      if (contextFromSession == null) {
         return null;
      }
      ...
      return (SecurityContext) contextFromSession;
   }

   //初次请求时创建一个新的SecurityContext实例
   protected SecurityContext generateNewContext() {
      return SecurityContextHolder.createEmptyContext();
   }

}

SecurityContextPersistenceFilter and HttpSessionSecurityContextRepository are used together to form the entrance of the entire call link of Spring Security. It is also obvious why it is placed at the very beginning. Subsequent filters will most likely rely on Session information and security context information.

Introduce UsernamePasswordAuthenticationFilter

Form authentication is the most commonly used authentication method. One of the most intuitive business scenarios is to allow users to enter their username and password in the form to log in. The UsernamePasswordAuthenticationFilter behind this plays a vital role in the entire Spring Security authentication system. character of.

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {
   //获取表单中的用户名和密码
   String username = obtainUsername(request);
   String password = obtainPassword(request);
   ...
   username = username.trim();
   //组装成username+password形式的token
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
         username, password);
   // Allow subclasses to set the "details" property
   setDetails(request, authRequest);
   //交给内部的AuthenticationManager去认证,并返回认证信息
   return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationFilter本身的代码只包含了上述这么一个方法,非常简略,而在其父类AbstractAuthenticationProcessingFilter中包含了大量的细节,值得我们分析:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {
    //包含了一个身份认证器
    private AuthenticationManager authenticationManager;
    //用于实现remeberMe
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private RequestMatcher requiresAuthenticationRequestMatcher;
    //这两个Handler很关键,分别代表了认证成功和失败相应的处理器
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
    
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        ...
        Authentication authResult;
        try {
            //此处实际上就是调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                //子类未完成认证,立刻返回
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        //在认证过程中可以直接抛出异常,在过滤器中,就像此处一样,进行捕获
        catch (InternalAuthenticationServiceException failed) {
            //内部服务异常
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        catch (AuthenticationException failed) {
            //认证失败
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        //认证成功
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        //注意,认证成功后过滤器把authResult结果也传递给了成功处理器
        successfulAuthentication(request, response, chain, authResult);
    }
    
}

The whole process is not difficult to understand. The main reason is that the authenticationManager is called internally to complete the authentication, and successfulAuthentication or unsuccessfulAuthentication is executed according to the authentication result. Friends who are interested can go and see the implementation classes of the two.

Introduce AnonymousAuthenticationFilter

Anonymous authentication filter, some people may think: Anonymous still have identity? My own understanding of the Anonymous identity is that Spirng Security has given an anonymous identity even to unauthenticated users for the sake of the unity of the overall logic. The location of the AnonymousAuthenticationFilter filter is also very scientific. It is located after commonly used identity authentication filters (such as UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, RememberMeAuthenticationFilter), which means that only after the above identity filters are executed, the SecurityContext still has no user information, and the AnonymousAuthenticationFilter This filter makes sense - based on the user's anonymity.

org.springframework.security.web.authentication.AnonymousAuthenticationFilter

public class AnonymousAuthenticationFilter extends GenericFilterBean implements
      InitializingBean {

   private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
   private String key;
   private Object principal;
   private List<GrantedAuthority> authorities;


   //自动创建一个"anonymousUser"的匿名用户,其具有ANONYMOUS角色
   public AnonymousAuthenticationFilter(String key) {
      this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
   }

   /**
    *
    * @param key key用来识别该过滤器创建的身份
    * @param principal principal代表匿名用户的身份
    * @param authorities authorities代表匿名用户的权限集合
    */
   public AnonymousAuthenticationFilter(String key, Object principal,
         List<GrantedAuthority> authorities) {
      Assert.hasLength(key, "key cannot be null or empty");
      Assert.notNull(principal, "Anonymous authentication principal must be set");
      Assert.notNull(authorities, "Anonymous authorities must be set");
      this.key = key;
      this.principal = principal;
      this.authorities = authorities;
   }

   ...

   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
         throws IOException, ServletException {
      //过滤器链都执行到匿名认证过滤器这儿了还没有身份信息,塞一个匿名身份进去
      if (SecurityContextHolder.getContext().getAuthentication() == null) {
         SecurityContextHolder.getContext().setAuthentication(
               createAuthentication((HttpServletRequest) req));
      }
      chain.doFilter(req, res);
   }

   protected Authentication createAuthentication(HttpServletRequest request) {
     //创建一个AnonymousAuthenticationToken
      AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
            principal, authorities);
      auth.setDetails(authenticationDetailsSource.buildDetails(request));

      return auth;
   }
   ...
}

In fact, by comparing AnonymousAuthenticationFilter and UsernamePasswordAuthenticationFilter, you can find some ways. UsernamePasswordAuthenticationToken corresponds to AnonymousAuthenticationToken. They are all authentication implementation classes, and Authentication is held by SecurityContextHolder (SecurityContext). Everything is connected in series.

Introduce ExceptionTranslationFilter

The ExceptionTranslationFilter exception translation filter is located behind the entire springSecurityFilterChain and is used to convert the exceptions that appear in the entire link and convert them. As the name suggests, the conversion means that it does not process itself. Generally, it only handles two types of exceptions: AccessDeniedException and AuthenticationException.

This filter is very important because it connects the exception in Java with the HTTP response, so that when handling exceptions, we don't have to think about what page to jump to when the password is wrong, how to lock the account, and only need to focus on our own business Logic, just throw the corresponding exception. If the filter detects an AuthenticationException, it will be handed over to the internal AuthenticationEntryPoint for processing. If an AccessDeniedException is detected, it is necessary to first determine whether the current user is an anonymous user. If it is an anonymous access, run the AuthenticationEntryPoint as before, otherwise it will be delegated to AccessDeniedHandler to process, and the default implementation of AccessDeniedHandler is AccessDeniedHandlerImpl. So the AuthenticationEntryPoint inside ExceptionTranslationFilter is crucial, as the name suggests: the entry point of authentication

public class ExceptionTranslationFilter extends GenericFilterBean {
  //处理异常转换的核心方法
  private void handleSpringSecurityException(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, RuntimeException exception)
        throws IOException, ServletException {
     if (exception instanceof AuthenticationException) {
           //重定向到登录端点
        sendStartAuthentication(request, response, chain,
              (AuthenticationException) exception);
     }
     else if (exception instanceof AccessDeniedException) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
          //重定向到登录端点
           sendStartAuthentication(
                 request,
                 response,
                 chain,
                 new InsufficientAuthenticationException(
                       "Full authentication is required to access this resource"));
        }
        else {
           //交给accessDeniedHandler处理
           accessDeniedHandler.handle(request, response,
                 (AccessDeniedException) exception);
        }
     }
  }
}

The rest is to understand AuthenticationEntryPoint and AccessDeniedHandler.

Selected several commonly used login endpoints, and introduced the first one as an example. You can guess from the name that after the authentication fails, the user is redirected to the login page. Remember how we configured the form login page in the first place?

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()//FormLoginConfigurer
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

oauth2.0

Why do you need oauth2.0

The main reason is that the login verification method provided by security is not easy to expand, and secondly, oauth2.0 is actually an enhanced version of security

  • 1. Provides four ways

  • Authorization code mode, account password mode, simplified mode (directly apply for a token and return the token, such as to the program, mainly when the application end has only the front end without a server), client mode (a mode that has nothing to do with the user, the relationship between the server and the server Inter-communication, such as api calls between internal systems)

  • Authorization code mode

  • client_id client id, obtained when creating the application

  • redirect_uri Redirect address, return a code to this address after authentication is completed

  • scope The scope of application permissions, such as reading and writing, etc.

  • state Random value, used for subsequent verification

  • response_type: just fill in the code

  • 2.oauth provides return parameter enhancement

  • That is, after we log in successfully, we can add some return parameters, such as avatar, permissions, menu, etc. By implementing tokenEnhancer, you can finally specify the bean

@AllArgsConstructor
public class ProcessJwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Object principal = authentication.getUserAuthentication().getPrincipal();

        Map<String, Object> additionalInfo = new HashMap<>(6);

        String openId; //openId
        String tenantId;//租户id
        //会员用户
        if (authentication.getPrincipal() != null && authentication.getPrincipal() instanceof MemberDetails) {
            MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
            openId = memberDetails.getOpenid();
            additionalInfo.put(TokenUtil.USER_ID, openId);
//            additionalInfo.put("sessionKey",memberDetails.getSessionKey());
            additionalInfo.put("infoAuth", StringUtil.isBlank(memberDetails.getAvatar())); //头像为空,提示获取头像信息 true为需要 false为不需要
            //管理员用户
        } else if (authentication.getPrincipal() != null && authentication.getPrincipal() instanceof ProcessUserDetails) {
            ProcessUserDetails adminUser = (ProcessUserDetails) principal;
            Map<String, Object> info = new HashMap<>(16);

            openId = Func.toStr(adminUser.getUserId());
            tenantId = Func.toStr(adminUser.getTenantId());
            info.put(TokenUtil.CLIENT_ID, TokenUtil.getClientIdFromHeader());
            info.put(TokenUtil.USER_ID, openId);
            info.put(TokenUtil.DEPT_ID, Func.toStr(adminUser.getDeptId()));
            info.put(TokenUtil.POST_ID, Func.toStr(adminUser.getPostId()));
            info.put(TokenUtil.ROLE_ID, Func.toStr(adminUser.getRoleId()));
            info.put(TokenUtil.TENANT_ID, tenantId);
            info.put(TokenUtil.OAUTH_ID, adminUser.getOauthId());
            info.put(TokenUtil.ACCOUNT, adminUser.getAccount());
            info.put(TokenUtil.USER_NAME, adminUser.getUsername());
            info.put(TokenUtil.NICK_NAME, adminUser.getName());
            info.put(TokenUtil.REAL_NAME, adminUser.getRealName());
            info.put(TokenUtil.ROLE_NAME, adminUser.getRoleName());
            info.put(TokenUtil.AVATAR, adminUser.getAvatar());
            info.put(TokenUtil.DETAIL, adminUser.getDetail());
            info.put(TokenUtil.LICENSE, TokenUtil.LICENSE_NAME);

            additionalInfo.put("auth", AesUtil.encryptToHex(JsonUtil.toJson(info), JwtSecretKey.getSecret32Key()));
            additionalInfo.put(TokenUtil.USER_ID, Func.toStr(adminUser.getUserId()));
            additionalInfo.put(TokenUtil.USER_NAME, adminUser.getUsername());
            additionalInfo.put(TokenUtil.NICK_NAME, adminUser.getName());
            additionalInfo.put(TokenUtil.ACCOUNT, adminUser.getAccount());
            additionalInfo.put(TokenUtil.AVATAR, adminUser.getAvatar());
            additionalInfo.put(TokenUtil.TENANT_ID, adminUser.getTenantId());
        }
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}
/**
 * 用于扩展jwt
 */
@Bean
@ConditionalOnMissingBean(name = "jwtTokenEnhancer")
public TokenEnhancer jwtTokenEnhancer() {
    return new ProcessJwtTokenEnhancer();
}
  • 3. Provides a custom token acquisition method, implemented by inheriting the AbstractTokenGranter class, then adding it to the tokenGranter collection, adding it to the collection through compositeTokenGranter, and finally adding it to endpoints.tokenGranter

//将所有自定义的tokenGranter加入到集合
public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager, final AuthorizationServerEndpointsConfigurer endpoints, RedisOpration redisOpration, SocialProperties socialProperties, CaptchaService captchaService, MemberClient memberClient, ShanyanService shanyanService) {
        // 默认tokenGranter集合
        List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
        // 增加验证码模式
        granters.add(new BlockCaptchaTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), redisOpration, captchaService));
        //增强第三方登录模式 小程序、app、公众号等
        granters.add(new OpenIdTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), memberClient, socialProperties, redisOpration));
        //增加短信登陆模式
        granters.add(new MobileTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), redisOpration, memberClient));
        granters.add(new ShanyanTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), memberClient, shanyanService));
        // 组合tokenGranter集合
        return new CompositeTokenGranter(granters);
}

//该方法是oauth2.0提供的
public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }
    
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }
    
    public void addTokenGranter(TokenGranter tokenGranter) {
        if (tokenGranter == null) {
            throw new IllegalArgumentException("Token granter is null");
        }
        tokenGranters.add(tokenGranter);
    }

}

Inherit the AuthorizationServerConfigurerAdapter class

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints.tokenStore(tokenStore)
            .authenticationManager(authenticationManager)
            .allowedTokenEndpointRequestMethods(HttpMethod.POST)
            .userDetailsService(userDetailsService)
            //设置自定义异常
            .exceptionTranslator(new CustomOauth2Exception());
    //扩展token返回结果
    if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancerList = new ArrayList<>();
        enhancerList.add(jwtTokenEnhancer);
        enhancerList.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(enhancerList);
        //jwt增强
        endpoints.tokenEnhancer(tokenEnhancerChain).accessTokenConverter(jwtAccessTokenConverter);
    }

    endpoints.reuseRefreshTokens(false);
    DefaultTokenServices defaultProcessTokenServices = createDefaultProcessTokenServices();
    defaultProcessTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    endpoints.tokenServices(defaultProcessTokenServices);
    //配置授权码模式code自定义方式处理
    endpoints.authorizationCodeServices(authorizationCodeServices());

    //获取自定义tokenGranter
    TokenGranter tokenGranter = ProcessTokenGranter.getTokenGranter(authenticationManager, endpoints, redisOpration, socialProperties, captchaService, memberClient, shanyanService);
    //配置端点
    endpoints
            .tokenGranter(tokenGranter);
}
  • 4. Provides a way to obtain user information based on token

  • Security provides securityContextHolder to obtain user information, oauth2.0 provides tokenStore class, you can call readAuthentication(token) to obtain user information, when I use security and oauth2.0, I used to obtain information through securityContextHolder, but did not get it

  • Secondly, the securityContextHolder of security can meet the needs in the case of a single service, but if we have multiple authorization services, the result is unknown, and if the ReadAuthentication method is used, no matter how many servers are used, user information can be obtained

  • 5. Provide a custom token header file, just implement TokenExtractor, then inherit ResourceServerConfigurerAdapter, add configure (ResourceServerSecurityConfigurer resources), that is, resources.tokenExtractor (token class)

@Slf4j
public class ProcessTokenExtractor implements TokenExtractor {

    @Override
    public Authentication extract(HttpServletRequest request) {
        String tokenValue = extractToken(request);
        if (tokenValue != null) {
            return new PreAuthenticatedAuthenticationToken(tokenValue, "");
        }
        return null;
    }

    protected String extractToken(HttpServletRequest request) {
        // first check the header...
        String token = extractHeaderToken(request);
        // bearer type allows a request parameter as well
        if (token == null) {
            log.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter(TokenConstant.HEADER);
            if (token == null) {
                log.debug("Token not found in request parameters.  Not an OAuth2 request.");
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
            }
        }

        return token;
    }

    /**
     * Extract the OAuth bearer token from a header.
     *
     * @param request The request.
     * @return The token, or null if no OAuth authorization header was supplied.
     */
    protected String extractHeaderToken(HttpServletRequest request) {
        //定义头文件读取token
        Enumeration<String> headers = request.getHeaders(TokenConstant.HEADER);
        while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
            String value = headers.nextElement();
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // Add this here for the auth details later. Would be better to change the signature of this method.
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
                        value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }

        return null;
    }
}

//该方法是继承ResouceServerConfigurerAdapter
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    //无权限异常
    resources
            //自定义token头
            .tokenExtractor(new ProcessTokenExtractor())
            //令牌失效提示
            .authenticationEntryPoint(resourceAuthenticationEntryPointException)
            //权限不足提示信息
            .accessDeniedHandler(accessDeniedHandlerException)
            //token
            .tokenServices(resourceServerTokenServices());
}

tokenStore analysis

1. Source code analysis

public interface TokenStore {

    //根据oauth2AccessToken去获取认证的信息
    OAuth2Authentication readAuthentication(OAuth2AccessToken token);

    //通过String类型的token去获取授权认证的信息
    OAuth2Authentication readAuthentication(String token);

    //以token为key,对认证信息进行存储
    void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);

    //通过token去获取访问令牌
    OAuth2AccessToken readAccessToken(String tokenValue);

    //移除token及认证信息
    void removeAccessToken(OAuth2AccessToken token);

    //存储刷新token,以refreshToken为key,对认证信息进行存储
    void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);

    //通过refreshToken去获取刷新token访问凭证
    OAuth2RefreshToken readRefreshToken(String tokenValue);

    //刷新token令牌的身份验证
    OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);

    //移除刷新token的身份
    void removeRefreshToken(OAuth2RefreshToken token);

    //刷新令牌使用后刷新得到token,该功能必须的,因此刷新令牌时不能用于创建无限数量的令牌
    void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);

    //通过认证信息检索已存在的令牌信息
    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

    //通过客户端id和用户名去检索访问令牌集合
    Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);

    //检索当前客户端id下所有token的集合
    Collection<OAuth2AccessToken> findTokensByClientId(String clientId);

}

2. Implementation of tokenStore

  • InMemoryTokenStore memory method

  • As the name suggests, this method is actually to store token-related information in the server memory. This method is only for the case of a single server, and a single server can be implemented using this method.

  • JdbcTokenStore database method

  • Using jdbc to store, analyze and refresh the token, if the number of users is too large, it is unbearable, because every request will call readAuthencation, but if the resource service is in each business system, it must be guaranteed The database is a single database, otherwise the relevant token information cannot be read

  • redisTokenStore redis method

  • To store, parse, and refresh tokens in the form of redis, although it satisfies the situation of a large number of users, it cannot satisfy the situation of multiple business systems with different redis, followed by the situation of a single redis with different libraries, which needs to be realized by itself and the redis connection established by itself

  • JwtTokenStore jwt method

  • The structure of jwt

  • header header

  • alg: The default is Hs256. It is also the default encryption algorithm of jwt

  • typ: type, which is jwt

  • Payload payload usually stores user-related information, but do not store sensitive information. Some websites have provided jwt analysis, which can be decrypted to get the information

  • The function of signature signature is to ensure that jwt has not been tampered with

  • Whether it is the native jwt or the jwt provided by oauth, it is actually the same. The token analysis includes claims. As long as the token is not tampered with, user information can be obtained in either way. This also shows that no matter whether the authorization verification or resource server is How many servers can finally get relevant information through token analysis. The disadvantage of jwtTokenStore#removeAccessToken, jwt cannot be deleted in a real sense, that is, when we log out and log in, we cannot log out in a real sense, so this method does not It can only be achieved through front-end collaboration and clearing the token header

@Override
public void removeAccessToken(OAuth2AccessToken token) {
        // gh-807 Approvals (if any) should only be removed when Refresh Tokens are removed (or expired)
}
    

oauth2.1

The difference between SSO single sign-on and oauth

  • Both SSO and oauth use tokens instead of user passwords to access applications. The process is similar, but the concept is different. SSO separates login authentication from business systems, uses an independent login center, and realizes that after logging in in the login center, all related All business systems can access resources without login

  • For oauth2.0, when I use CSDN, I don’t want to use WeChat or qq to authorize login when registering. I don’t have an account password in this CSDN during this process. A new CSDN account will be generated only after the authorization login is successful, and we use WeChat or qq The account password of qq is stored in the login center or in the qq and WeChat servers, which is the so-called use of tokens instead of account passwords to access applications

  • SSO is an idea, and the CAS framework is a framework implementation of this idea

sso overview

  • general process

  • When a user enters a business system and finds that the user is not logged in, the user will be redirected to the single sign-on system with relevant parameters of its own address

  • Redirect to the single sign-on system, the system will check whether the user is logged in, this is also the first interface of the sso system, if not logged in, redirect to the login interface, if logged in, set the global session, and redirect to the business system.

  • After the user fills in the password and submits the login, if it is correct, it will be directly redirected to the business system and bring the token issued by the sso system

  • After that, all interactions can be done with sessionId to interact with the business system, and the business system takes the token to request the sso system to obtain user information.

  • Common SSOs include Taobao, Taobao home page, Dianjuhuasuan, Tmall, etc., no need to log in again

Overview of oauth2.0

The process is roughly similar to that of sso, but there are several roles in oauth2.0, such as authorization server, resource server, and application. When we use sso, we don't need the role of resource server. Authorization server and application are enough.

The authorization server is used for authentication. For applications, it refers to each application system. We only need to get the user information and the permissions of the user after successful login.

Take CSDN as an example,

  • When we log in to CSDNAPP, we click WeChat orqq to authorize. The csdn here is similar to the business system, and the WeChat authorization service is similar to the single sign-on system.

  • After that, WeChat or qq will return a confirmation authorization page, similar to the login page, this page is provided by WeChat or qq, not csdn

  • The user confirms the authorization. In fact, he brings the token to the qq or WeChat server. After submitting, WeChat or qq returns a code and redirects to CSDN.

  • CSDN拿到这个code就去获取微信或者qq服务器去获取token,业务系统通过token就可以获取相关信息了,具体的话其实是获取code的,通过code去获取微信或qq用户信息相关字段,只不过是加密的,最后通过code和微信token换取sessionInfo就可以解密得到用户基本信息了,

Guess you like

Origin blog.csdn.net/qq_31671187/article/details/128811888