On paper come zhongjue shallow, realize that this matter to practice.

wedge

For those of you who know a little about SpringSecurity or have run a simple demo but don’t understand the whole process, this is also a good introduction for those who are interested in SpringSecurity. The sample code is also well annotated. When we do the system, generally do the first module is the authentication and authorization module, because this is a system entrance, is also the most important and basic part of a system, in the authentication and authorization service design set up, the rest of the module to secure access. Shiro and Spring Security are common frameworks for authentication and authorization in the market, but most companies choose to develop their own. Since I’ve seen a lot of Spring Security tutorials before, but I don’t think they are very good, so I came up with the idea of sharing them when I was working on Spring Security these two days, hoping to help people who are interested.

Spring Security framework we mainly use it to solve an authentication and authorization function, so my article will be divided into two parts:

  • Part I Certification (This section)
  • Part II Authorization (in next article)

I will use a Spring Security + JWT + cache demo for you to show what I want to talk about, after all, the brain of things to reflect in concrete things can be more intuitive for you to understand to recognize. When learning a new thing, I recommend using the top-down learning method, so that you can better understand the new thing, rather than the blind men feeling the elephant.

Note: Only user authentication and authorization are involved and third-party authorization such as OAuth2 is not involved.

1. Workflow of 📖SpringSecurity

To get started with Spring Security, it’s important to understand how it works, because unlike a toolkit, you have to understand it and then customize its usage.

Let’s start by looking at how it works: The official Spring Security documentation reads:

Spring Security’s web infrastructure is based entirely on standard servlet filters.

The Web foundation of Spring Security is Filters.

Spring Security is designed to process Web requests through layers of Filters.

Put it in real Spring Security, and put it in words like this:

A Web request passes through a filter chain. During the process of passing through the filter chain, authentication and authorization are completed. If the request is found to be unauthenticated or unauthorized, exceptions will be thrown according to the permissions of the protected API, and the exception handler will handle these exceptions.

If you use a picture, you can draw it like this. This is a picture I found on Baidu:



As shown in the figure above, a request requesting access to the API passes through the filters in the blue wireframe from left to right, where the green filter is for authentication, the blue filter is for exception handling, and the orange filter is for authorization.

The two green filters in the figure are not going to be mentioned today, because they are the two filters built in Spring Security for form authentication and Basic authentication, and our demo is JWT authentication, so they are not used.

If you haveSpring SecurityYou should know that there are two names in the configurationformLoginandhttpBasicIf you turn on both of these items in the configuration, the above filters are turned on.



  • formLoginCorresponds to your form form authentication way, namely UsernamePasswordAuthenticationFilter.
  • httpBasicCorresponds to the Basic authentication way, namely BasicAuthenticationFilter.

In other words, you need to configure these two authentication methods to add them to the filter chain, otherwise they will not be added to the filter chain.

Because Spring Security does not provide JWT authentication in its own filter, we will write a JWT authentication filter in the demo, and then put it in the green position for authentication.

2. 📝 Important concepts of SpringSecurity

Now that we know the general workflow of Spring Security, we need to know some very important concepts or components:

  • SecurityContext: context object,AuthenticationObjects are going to go inside.
  • SecurityContextHolder: Static utility class used to get the context object.
  • Authentication: indicates the Authentication interface, which defines the data form of the Authentication object.
  • AuthenticationManager: Used for verificationAuthentication, return a completed authenticationAuthenticationObject.

1.SecurityContext

Context object, where authenticated data is stored. The interface is defined as follows:

public interface SecurityContext extends Serializable {
 // Obtain the Authentication object
 Authentication getAuthentication(a);

 // Insert the Authentication object
 void setAuthentication(Authentication authentication); } Copy the code

There are only two methods in this interface. The main function is to get or set Authentication.

2. SecurityContextHolder

public class SecurityContextHolder {

 public static void clearContext(a) {
  strategy.clearContext();
 }
  public static SecurityContext getContext(a) {  return strategy.getContext();  }   public static void setContext(SecurityContext context) {  strategy.setContext(context);  }  } Copy the code

It’s a utility class for SecurityContext, which is used to get or set or clear SecurityContext, and by default it stores all the data in the current thread.

3. Authentication

public interface Authentication extends Principal.Serializable {
 
 Collection<? extends GrantedAuthority> getAuthorities();
 Object getCredentials(a);
 Object getDetails(a);
 Object getPrincipal(a);  boolean isAuthenticated(a);  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; } Copy the code

The results of these methods are as follows:

  • GetAuthorities: orities get user permissions. Typically, they get the user’s role information.
  • GetCredentials: Obtains the information that proves user authentication. Usually, the information such as the password is obtained.
  • GetDetails: Get additional information about the user (this information can be in our user table).
  • GetPrincipal: Obtains the user identity information. In the case of no authentication, the user name is obtained; in the case of authentication, the UserDetails are obtained.
  • isAuthenticated: Get the currentAuthenticationWhether the system is authenticated.
  • setAuthenticated: Set the currentAuthenticationAuthenticated or not (true or False).

Authentication simply defines what a data form should look like for data that has been authenticated by SpringSecurity, with permissions, passwords, identity information, and additional information.

4. AuthenticationManager

public interface AuthenticationManager {
 // Authentication method
 Authentication authenticate(Authentication authentication)
   throws AuthenticationException;
}
Copy the code

AuthenticationManager defines an Authentication method that passes in an unauthenticated Authentication and returns an authenticated Authentication. The default implementation class is ProviderManager. Next, you can imagine how these four parts can be connected together to form the Spring Security authentication process: 1. 👉 first a request comes in with the identity information 2. 👉 is authenticated by the AuthenticationManager, 3. 4. 👉 finally puts the authenticated information into the SecurityContext.

3. 📃 Code preparations

Before we really start to talk about our authentication code, we first need to import the necessary dependencies. Database-related dependencies can choose what JDBC framework they want. Here I use Myabtis-Plus, the secondary development of Chinese people.

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

 <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>  </dependency>   <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-validation</artifactId>  </dependency>   <dependency>  <groupId>io.jsonwebtoken</groupId>  <artifactId>jjwt</artifactId> The < version > 0.9.0 < / version > </dependency>   <dependency>  <groupId>com.baomidou</groupId>  <artifactId>mybatis-plus-boot-starter</artifactId> The < version > 3.3.0 < / version > </dependency>   <dependency>  <groupId>mysql</groupId>  <artifactId>mysql-connector-java</artifactId> The < version > 5.1.47 < / version > </dependency> Copy the code

Next, we need to define the required components. Since my spring-boot is 2.x, we have to define our own encryptor:

1. Define the crypto Bean

 @Bean
    public PasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }
Copy the code

This Bean is not necessary, Spring Security will use the cryptographer we defined for authentication operations, and an exception will occur if it does not.

2. Define the AuthenticationManager

@Bean
    public AuthenticationManager authenticationManager(a) throws Exception {
        return super.authenticationManager();
    }
Copy the code

We declare Spring Security’s authenticationManager as a Bean. We declare Spring Security’s authenticationManager as a Bean. We declare Spring Security’s authenticationManager as a Bean to authenticate us.

3. Implement UserDetailsService

public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;
 @Override  public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {  log.debug("Start login authentication, username: {}",s);   // Authenticate the user by user name  QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();  queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);  UserInfo userInfo = userService.getOne(queryWrapper);  if (userInfo == null) {  throw new UsernameNotFoundException("User name does not exist, login failed.");  }   // Build the UserDetail object  UserDetail userDetail = new UserDetail();  userDetail.setUserInfo(userInfo);  List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());  userDetail.setRoleInfoList(roleInfoList);  return userDetail;  } } Copy the code

Implement the UserDetailsService abstract method and return a UserDetails object. During authentication, SpringSecurity will call this method to access the database and search for the user. The logic can be customized, either from the database or from the cache. But we need to assemble the user information and permission information we queried into a UserDetails return.

UserDetails is also an interface that defines the data form, which is used to save the data we find out from the database. Its function is mainly to verify the account status and obtain the permission, and the specific implementation can refer to the code of my warehouse.

4. TokenUtil

Since we are JWT authentication mode, we also need a utility class to help us manipulate tokens. Generally speaking, it has the following three methods:

  • Create a token
  • Authentication token
  • Anti-parse the information in the token

In my code below, the JwtProvider acts as a Token utility class, which can be implemented in my repository code.

✍ code concrete implementation

After the previous explanation, you should know that using SpringSecurity for JWT authentication requires you to write a filter to do the JWT validation, and then put the filter in the green section. Before we can write this filter, we need to perform an authentication operation, because we need to access the authentication interface to get the token before we can put the token on the request header for the next request. If you don’t understand that, don’t worry, just keep reading and I’ll sort it out again at the end of the video.

1. Authentication method

When accessing a system, the authentication method is usually the first to be accessed. Here I have written down the simplest authentication steps, because in the real system we have to write the login record, the front door password decrypt and so on.

@Override
    public ApiResult login(String loginAccount, String password) {
        Create UsernamePasswordAuthenticationToken / / 1
        UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
        / / 2 certification
 Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);  // 3 Save the authentication information  SecurityContextHolder.getContext().setAuthentication(authentication);  // 4 Generate a custom token  UserDetail userDetail = (UserDetail) authentication.getPrincipal();  AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());   // 5 Put into the cache  caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);  return ApiResult.ok(accessToken);  } Copy the code

There are five steps, and only the first four are unfamiliar:

  1. Passing in the user name and password creates aUsernamePasswordAuthenticationTokenObject, that’s what we talked about earlierAuthenticationWith the user name and password as the construction parameters, this object is the unauthenticated object that we createdAuthenticationObject.
  2. Using the Bean we declared earlier –authenticationManagerCall it theauthenticateMethod for authentication and returns an authenticated oneAuthenticationObject.
  3. When the authentication is complete and there is no exception, it will go to the third step and useSecurityContextHolderTo obtainSecurityContextAfter the certification will be completedAuthenticationObject to put in the context object.
  4. fromAuthenticationTo get ourUserDetailsThe object, we talked about earlier, certifiedAuthenticationObject that calls itgetPrincipal()Method that we assembled from our previous database queryUserDetailsObject, and create the token.
  5. theUserDetailsObjects are placed in the cache for later filters.

So even if finished, feeling is very simple, because certification operation will be dominated by the authenticationManager. The authenticate () to help us to complete.


Let’s take a look at the source code to see how Spring Security does this for us (omitted) :

// AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication){

  // Check whether the user name is in the Authentication object that is not authenticated
 String username = (authentication.getPrincipal() == null)?"NONE_PROVIDED"  : authentication.getName();   boolean cacheWasUsed = true;  // Select * from cache where user name is XXX  UserDetails user = this.userCache.getUserFromCache(username);   // If not, go to this method  if (user == null) {  cacheWasUsed = false;   try {  // Call our loadUserByUsername method which overrides the UserDetailsService  // Get the UserDetails object we assembled ourselves  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 {  // Verify whether the account is disabled  preAuthenticationChecks.check(user);  // Verify that the password found by the database is consistent with the password we passed in  additionalAuthenticationChecks(user,  (UsernamePasswordAuthenticationToken) authentication);  }   } Copy the code

After looking at the source code, you will find that as we usually write, the main logic is to check the database and then compare the password.



After login, the effect is as follows:





After we return the token, the next time we request another API, we need to include the token in the request header, according to the JWT standard.

JWT filter

After we have the token, we need to put the filter in the filter chain to resolve the token. Since we do not have a session, we will resolve the current user based on the token in the request every time we try to identify which user the request is from. So we need a filter to intercept all requests, as we also said that the filter we will be in the green part is used to replace UsernamePasswordAuthenticationFilter, so we create a new JwtAuthenticationTokenFilter, Then register it as a Bean and add this when writing the configuration file:

@Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(a) {
        return new JwtAuthenticationTokenFilter();
    }

@Override  protected void configure(HttpSecurity http) throws Exception {  http.addFilterBefore(jwtAuthenticationTokenFilter(),  UsernamePasswordAuthenticationFilter.class);  } Copy the code

AddFilterBefore semantics is to add a Filter to the XXXFilter before, here is to put the JwtAuthenticationTokenFilter UsernamePasswordAuthenticationFilter before, Because filter execution is also sequential, we must put our filter in the green part of the filter chain to play the effect of automatic authentication. Then we can see the specific implementation JwtAuthenticationTokenFilter:

@Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
 @NotNull FilterChain chain) throws ServletException, IOException {
        log.info("The JWT filter validates the request header token for automatic login...");
  // Obtain the information in the Authorization request header  String authToken = jwtProvider.getToken(request);   // Check if the content is false and if it is a false start  if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {  // Bearer removes the token prefix and obtains real tokens  authToken = authToken.substring(jwtProperties.getTokenPrefix().length());   // Get the login account in the token  String loginAccount = jwtProvider.getSubjectFromToken(authToken);   if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {  // There is no need to log in again.  UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);   // After receiving the user information, verify the user information and token  if(userDetails ! =null && jwtProvider.validateToken(authToken, userDetails)) {   // Assemble the authentication object. The construction parameters are Principal Credentials and Authorities  // The grant Authorities method is used in the following interceptors  UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());   // Put the authentication information into the context object  SecurityContextHolder.getContext().setAuthentication(authentication);   log.info("JWT filter automatically logon successfully by verifying request header token, user: {}", userDetails.getUsername());  }  }  }   chain.doFilter(request, response);  } Copy the code

Although the steps in the code are very detailed, but probably because the code is too long to read, I will simply talk about it, or you can directly go to the warehouse to check the source code:

  1. getAuthorizationToken information corresponding to the request header
  2. Removal of token head (Bearer)
  3. Parse the token and get the login account we put in it
  4. Since we’ve logged in before, we’ll take ours directly from the cacheUserDetailInformation can be
  5. Check whether the UserDetail is null and whether the token has expired.UserDetailWhether the user name and token are consistent.
  6. Assemble aauthenticationObject, put it in the context object, so that the filters that follow see that we have it in the context objectauthenticationObject, it’s like we’ve already certified it.

This way, every request with the correct token will be found and put into the context object. We can use SecurityContextHolder to get the Authentication object in the context object easily.

When we’re done, we start our demo, and we can see the following filters in the filter chain, of which our custom is the fifth one:

🐱🏍 : The account information and role information we get after logging in will be put into the cache. When the request with token comes, we will take it out of the cache and put it into the context object again.

Combined with the authentication method, our logical chain becomes:

Log in to 👉 and get token👉. Request to carry token👉 with JWT filter, intercept 👉, verify token👉. Put the object found in the cache into the context

After that, our logic for authentication is complete.

4. 💡 code optimization

With authentication and JWT filters complete, the JWT project is actually ready to run and achieve the desired effect. If we want to make the program more robust, we need to add some accessibility features to make the code more friendly.

1. Failed to authenticate the processor



This handler is triggered when the user is not logged in or the token resolution fails, returning an invalid access result.



2. Processor permissions are insufficient



When the user’s own permissions do not meet the requirements for access to the API, this handler is triggered, returning a result of insufficient permissions.



3. Exit method



User exit is usually a matter of clearing the context object and the cache, or you can do an add-on, which is required.

4. The token refresh



JWT project token refresh is also necessary. The main method of refreshing the token is put in the token tool class. After refreshing, you need to reload the cache again.

Afterword.






Spring Security is a bit of a challenge to get started. When I first learned about it, I saw a tutorial from Silicon Valley. The lecturer in that video combined it with Thymeleaf, which led to many blogs on the Internet talking about Spring Security in this way. Instead of paying attention to the anterior and posterior separation. Also have do tutorial is directly inherited UsernamePasswordAuthenticationFilter filter, this method is feasible, but we know the whole operation process after you know it is not necessary to do so, don’t need to carry on XXX, Just write a filter and put it there.














This article code: code cloud address GitHub address