Go deep into the smallest storage unit of InnoDB storage engine and analyze MySQL. In fact, indexing is not difficult

1, Foreword

Index can be said to be a necessary skill point for every engineer. Understanding the principle of index is very important for writing high-quality SQL. Today we will understand the principle of index from 0 to 1. I believe you will have a deeper understanding not only of index, but also of the "page" of the smallest storage unit of InnoDB storage engine in MySQL

Starting from actual needs

The following user table is assumed:

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` int(11) DEFAULT NULL COMMENT 'full name',
  `age` tinyint(3) unsigned DEFAULT NULL COMMENT 'Age',
  `height` int(11) DEFAULT NULL COMMENT 'height',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='User table';

We can see that the storage engine uses InnoDB. Let's first look at the SQL statements commonly used in this table. After all, the technology is to serve business needs,

1. select * from user where id = xxx
2. select * from user order by id asc/desc
3. select * from user where age = xxx
4. select age from user where age = xxx
5. select age from user order by age asc/desc

Since we want to query, let's insert some data first. After all, how can we query without data

insert into user ('name', 'age', 'height') values ('Zhang San', 20, 170);
insert into user ('name', 'age', 'height') values ('Li Si', 21, 171);
insert into user ('name', 'age', 'height') values ('Wang Wu', 22, 172);
insert into user ('name', 'age', 'height') values ('Zhao Liu', 23, 173);
insert into user ('name', 'age', 'height') values ('Qian Qi', 24, 174);

The data in the inserted table is as follows:

I wonder if you found that we did not specify an ID value when inserting, but InnoDB adds an ID value for each record by default, and the ID value is incremented. For each record inserted, the ID is incremented by 1. Why should the id be incremented? It is mainly for query convenience. Each record is connected by a linked list in the order of ID from small to large, In this way, each time you find the value of id = xxx, you can start with id = 1 and look back in turn

Now suppose we want to execute the following SQL statement, how will MySQL query

select * from user where id = 3

page

As mentioned earlier, first read the record with the smallest ID, i.e. id = 1, read one record at a time, compare its ID value with the value to be queried, and read the record three times continuously, so record 3 is found. Note that this reading operation first needs to read the record stored on the disk to internal memory, and then compare the ID, and read it from the disk to internal memory to calculate an IO, In other words, three IOS are generated in this process. If only a few records are OK, but if the number of records to be compared is large, it is a very serious challenge to the performance. If I want to query the records with id = 100, won't I generate 100 IOS? Since the bottleneck lies in io, how can we improve it? It's very simple. Our current design can only read one record per io. If we can read 100 or more records per IO, it will only generate one io. The idea behind this is the principle of program Locality: when a certain item of data is used, the adjacent data is likely to be used, So simply load the dependent data together (you start reading from id = 1, which is likely to use the elements immediately following id = 1, so simply load the records with id = 1 ~ id = 100)

Of course, reading records of one IO is not the more the better. You can't load a lot of irrelevant data into memory for one query record, which will cause a great waste of resources. Therefore, we adopted a compromise scheme. We stipulated that 16 K data should be read by one IO, assuming 100 data, In this way, if we want to query the records with id = 100, only one IO read (records with id=1~id=100) is generated, which improves the performance by 100 times compared with the original 100 io

We call this 16KB record combination one page

Page directory

I / O will read one page at a time, and then look up the records in the page in memory. Looking up the records in memory is much faster than the disk, but we are still not satisfied, because if you want to look up the records with id=100, you should first compare the records with id = 1, then id=2,..., id=100. You need to compare them 100 times. Can you be faster?

You can refer to binary search. First find id = (1+100)/2 = 50. Because 50 < 100, then check in 50 ~ 100 records, and then check in 75 ~ 100 records. In this way, you can find records with id = 100 times after 7 times, which improves a lot of performance compared with the original 100 times. But now the problem comes. The first time you find a record with id=50, you have to traverse 50 times from id = 1 to find it. Can you locate the record with id=50 at once? If not, even the first time you search from id = 30 or 40

What data structure can meet this demand? Remember to skip the table, extract one every n elements to form a primary index, and form a secondary index every 2*n elements...

As shown in the figure, taking the establishment of a primary index as an example, we first look up in the primary index, locate it in the primary index, and then look it up in the linked list. For example, if we want to find the number 7, we need to compare it seven times if we don't skip the list and directly look it up in the linked list. If we use the skip table, we first look it up in the primary index and find that we only need to compare it three times, reducing it by four times, Therefore, we can use the idea of skip table to reduce the number of queries. The specific operations are as follows: every four elements form a group to form a slot. The slot only records the record with the largest element in this group and how many records there are in this group

Now suppose we want to locate the record with id = 9. What should we do? It's very simple: first locate the slot in which the record is located, and then traverse the elements in this slot

  1. To locate the slot, first take the ID corresponding to the smallest slot and the largest slot (4 and 12 respectively). First, through binary search, take their middle value as (4 + 12) / 2 = 8, 8 is less than 9, and the maximum ID of slot 2 is 12. Therefore, it can be seen that the id = 9 is recorded in slot 2
  2. Traverse the elements in slot 2. Now the problem arises. We know that each record constitutes a single linked list, and each slot points to the maximum id value in this group. How can we traverse from the first element of the slot? Very simple, it is not enough to traverse from slot 1, because the next element it points to is the starting element of slot 2, After traversal, we found that the first element of slot 2 is the element with id 9 we found

We can see that our elements are quickly located in the page in this way. MySQL stipulates that there are 1 ~ 8 elements in each slot, so as long as the slot is located, the remaining comparison is not a problem. Of course, the records loaded on one page are limited after all. If the page is full, another page should be opened to load the records, Pages are connected through a linked list, but pay attention to the following figure. Why use a two-way linked list? Don't forget the two query conditions "order by id asc" and "order by id desc" listed at the beginning, that is, records need to support both positive and negative search, which is why a two-way linked list is used

Birth of B + tree

Now the problem arises. If there are many pages, how to locate the element? If the element happens to be in the first few pages, it's OK. It's a big deal. It's also fast to traverse the first few pages. However, if you want to find an element with id = 100w, you have to traverse 1w pages (assuming 100 records per page), which is obviously unacceptable. How to improve it, In fact, the previously created on page directory has inspired us. Since we can first locate the slot where the element is located and then find it by creating a page directory for records on a page, can we first locate the page where the element is located for multiple pages, that is, we can also create a directory for pages. Each record in this directory corresponds to the page and the smallest record in the page, Of course, this directory also exists in the form of pages. In order to facilitate differentiation, we call the page corresponding to the directory generated for the page as the directory page, and the page previously storing the complete record as the data page

Voice over: like the data page, the table of contents page also has slots inside. The above is not drawn for the convenience of display. Except for the recorded data, the other structures of the table of contents page and data are consistent

Now, if you want to find a record with id = xxx, it is very simple. Just locate its starting page in the directory page first, and then find it in turn. Since there are slots in both the directory page and the data page, it is very fast to locate the page number of the directory page and the records in the data page.

Of course, with the increase of pages, there are more and more records stored in the directory page, and the directory page will eventually be full. Then build another directory page, so now the problem comes, how to locate the directory page with the id you want to find? Just formulate the directory page for the directory page again, as follows

What do you think of when you see the structure above? Yes, this is a B + tree! Here, I believe you have understood the evolution of the B + tree and its principle. You can see that the B + tree has three layers. We call the directory page at the top as the root node and the page storing complete records at the bottom as the leaf node,

Now let's look at how to find the record with id = 55. First, we will load the root node and find that it should be found in the page of page 30, so we load page 30 and find that it should be found in page 4 in page 30, so we load page 4 into memory again, and then traverse the search in page 4 in turn. We can see that we have experienced IO for a total of 3 times (the B + tree has several layers, and there will be several IOS). After reading the page, it will be cached in memory. If you hit the page in memory, it will be directly obtained from memory. Someone may ask, if the B + tree has many layers, there may be many IOS. Let's simply calculate. Suppose that the data page can store 100 records and the directory page can store 1000 records (the directory page can store more records because it only stores primary keys and does not store complete data)

  • If the B + tree has only one layer, that is, only one node for storing user records, it can store up to 100 records.
  • If the B + tree has 2 layers, it can store up to 1000 × 100 = 100000 records.
  • If the B + tree has 3 layers, it can store up to 1000 × one thousand × 100 = 100000000 records.
  • If the B + tree has 4 layers, it can store up to 1000 × one thousand × one thousand × 100 = 1000000000 records!

Therefore, generally, layer 3 ~ 4 B + trees are sufficient to meet our requirements, and will be cached in memory after each reading (of course, they will be replaced out of memory according to certain algorithms). Therefore, on the whole, layer 3 ~ 4 B + trees are sufficient to meet our requirements

Clustered index and non clustered index

I believe you have found that the example of B + tree mentioned above is for id, that is, the index of primary key. It is not difficult to find that the leaf node in the primary key index stores complete SQL records. We call this index with complete records as cluster index. As long as you define the primary key, the primary key index is cluster index.

So what is the form of the index created by a non primary key column? The form of the non leaf node is exactly the same, but the storage of the leaf node is somewhat different. The non primary key column index stores the index column and the primary key value on the leaf node. For example, if we assume that we have established an index on the column age, its index tree is as follows

You can see that the non leaf node saves "age value + page number", while the leaf node saves "age value + primary key value". You may be confused. How does the following SQL get the complete record

select * from user where age = xxx

In the first step, we all know that the above SQL can hit the index corresponding to the age column, and then find the corresponding record (if any) on the leaf node However, the records on the leaf node only have the age and id columns, and you use select *, which means you need to find all the column information of the user. The answer is to find the complete record corresponding to the id in the cluster index according to the obtained id. this is what we call the back table. If there are many back tables, it will obviously cause some performance problems, because the IDs may be distributed in different places In the same page, this means that different pages should be read from disk into memory. These pages are probably not adjacent, which means that a large number of random IO will be caused, which will seriously affect the performance. Seeing this, I believe it is not difficult for you to understand a high-frequency interview question: why did you set the index, but still cause the full table scan? One of the reasons is that although the index was hit, but After the leaf node queries the records, it needs to return a large number of tables, which leads the optimizer to think that this situation is not as fast as full table scanning

Some people may ask why the secondary index does not store complete records. Of course, it is to save space. After all, complete data consumes a lot of space. If each index is added to store additional complete records, it will cause a lot of data redundancy.

How to avoid this situation? Index coverage. If the following SQL meets your needs, the following form is recommended

select age from user where age = xxx
select age,id from user where age = xxx

It is not difficult to find that the characteristic of this SQL is that the column (age) to be obtained is the index column itself (including id). In this way, after finding the corresponding records on the leaf node according to the age index, since the records themselves contain these columns, there is no need to return to the table, which can improve the performance

Disk read ahead

Next, let's discuss a problem that many people on the Internet can't understand. We know that the operating system manages memory in pages. In Linux, the size of a page is 4 KB by default, that is, whether loading data from disk to memory or writing memory back to disk, the operating system will operate in pages, Even if you write only one byte to an empty file, the operating system will allocate a page size (4 KB)

As shown in the figure, two byte s are written to the disk, but the operating system still allocates a page (4 KB) size to it

innoDB is also stored and read in pages, and the default size of innoDB pages is 16 KB. Many people on the Internet wonder whether this means that it needs to execute IO 4 times to read innoDB pages? No, only one IO is needed. Why? This requires an understanding of how disk reading works

Disk construction

First, let's look at the physical structure of the disk

The main components inside the hard disk are the disk disk, the drive magnetic arm, the read-write head and the rotating shaft. The data is mainly written on the disk of the disk. The disk is composed of several sectors. The data writing and reading are based on the sectors. In addition, the disk is divided into several concentric circles with the center of the disk as the center of the circle. Each "line" dividing the circle is called the track

So how to read and write data? There are three main steps

  1. Seek: since the data is stored in the sector, we first need to know which sector it is in. This requires the magnetic head to move to the track where the sector is located. We call it seek time. The average seek time is generally 3-15ms
  2. Rotation delay: when the disk moves to the disk where the sector is located, the magnetic head is not aligned with the sector corresponding to the data we want. Therefore, we need to wait for the disk to rotate for a moment until the sector corresponding to the data we want falls under the magnetic head. The rotation delay depends on the disk speed, which is usually expressed as 1 / 2 of the time required for the disk to rotate for one cycle. For example, the average rotation delay of 7200rpm disks is about 60*1000/7200/2 = 4.17ms, while the average rotation delay of 15000rpm disks is 2ms
  3. Data transmission: after the previous two steps, the magnetic head finally starts to read and write data. At present, IDE/ATA can reach 133MB/s, SATA II can reach 300MB/s interface data transmission rate, and the data transmission time is usually much less than that of the first two parts. Negligible

Note that there is a premise for neglect in data transmission, that is, it is necessary to read continuous adjacent sectors, that is, sequential io. The read and write speed of disk sequential IO can be comparable to or even exceed the random io of memory, so this part of time can be ignored, (the well-known reason why Kafka has strong performance is that it uses the sequential read-write of the disk). However, if the data to be read is distributed in different sectors, it will become random io. Random IO undoubtedly increases the seek time and rotation delay, and the performance is very worrying (a typical example is that a large number of IDS are distributed on different pages during the table return mentioned above, resulting in a large number of random IO)

As shown in the figure: the picture is from the performance comparison chart on the famous academic journal ACM Queue. It can be seen that the speed of Sequential Disk IO is faster than Random memory

Then why is reading a page in innoDB an IO? I believe you have guessed, because this page is allocated continuously, which means that their sectors are adjacent, so it is sequential io

The operating system manages memory on a page by page basis. It can load multiple pages at a time. The page size of innoDB is 16KB, which is just 4KB of the operating system page Therefore, you can specify to read four operating system pages continuously at the starting address of reading, that is, 16 KB. This is what we call disk pre reading. So far, I believe it is not difficult for you to understand why reading a page is actually only one IO rather than four times

summary

After reading this article, I believe you can understand the origin of the index. In addition, you should also know a lot about the performance improvement of page and disk pre reading. In fact, the page structure of MySQL is slightly different from the structure we deduced, but it does not affect the overall understanding.

The four authorization modes implemented by Spring Security OAuth2 by default often fail to meet expectations in actual application scenarios, such as the following requirements:

  1. The authorization object is divided into multiple user systems, such as system users and member users;
  2. Add a verification code verification on the basis of password authorization mode;
  3. Realize mobile phone and SMS verification code login based on Spring Security OAuth2;
  4. Authorized login of wechat applet based on Spring Security OAuth2.

I believe you will encounter but not limited to the above scenarios. There are also many articles on the extension of Spring Security OAuth2 authorization mode on the Internet, but there are some common problems of incompleteness and complex implementation, which once made you feel that Spring Security OAuth2 is difficult. Spring also provides many extension points on the basis of realizing core functions, as well as Spring Security OAuth2 This article will help dispel its difficult misunderstanding.

This chapter will expand the Spring Security OAuth2 authorization mode based on actual combat and supplemented by principles, in the principle of comprehensive and minimum changes. The contents of this chapter are as follows:

  1. Spring Cloud Gateway microservice gateway WebFlux integrates Google verification code   Kaptchaï¼›
  2. SpringBoot integrates Alibaba cloud SMS service;
  3. Analysis of the underlying source code of Spring Security OAuth2 authentication and authorization mode;
  4. Spring Security OAuth2 extended authentication code authorization mode;
  5. Spring Security OAuth2 extends the authorization mode of SMS verification code;
  6. Spring Security OAuth2 extends wechat authorization mode;
  7. Spring Security OAuth2 multi-user system refresh mode;
  8. Vue element admin background management front-end login access verification code authorization mode;
  9. Uni app wechat applet login and access wechat authorization mode;
  10. Uni app H5, mobile terminal mobile phone verification code login access SMS verification code authorization mode.

Let's make a very important statement first. This article covers all code addresses:

entry name

Code cloud (Gitee)

GitHub

Microservice background

youlai-mall

youlai-mall

Management front end

mall-admin-web

mall-admin-web

Wechat Applet / H5/Android/IOS

mall-app

mall-app

Because it involves a lot of content, it is impossible to completely post all the code in the article, but rest assured that the source code is all online, and the same documents are available

2, Verification code authorization mode

1. Principle

The verification code authorization mode is to add a verification code verification based on the password mode. If you have the mentality that no matter what Kung Fu, what can win you is good Kung Fu, you can use the filter, but if you can't think about it, try to expand it.

Because it is an extension based on password authorization mode, let's first understand the process of password authorization mode. Because the implementation principles of other authorization modes and password modes are the same, after understanding the password authorization mode, other authorization modes, including how to expand, are familiar.

Password mode process:   According to the request parameter grant_ The value password of type matches the authorizer
ResourceOwnerPasswordTokenGraner: the authorizer delegates to the authentication Provider manager ProviderManager and matches the Provider DaoAuthenticationProvider according to the token type. The Provider obtains the user authentication information and the user information requested by the client from the database to interpret the authentication password. After passing the authentication, it returns the token to the client.

The following sequence diagram of password authorization mode shows the key classes and methods. You should know the process after walking through the breakpoint several times.

The sequence diagram of the authentication code authorization mode is as follows. Carefully compare the differences between the authentication code authorization mode and the password authorization mode.

The comparison shows that the difference between the two is basically the difference between the authorizer and the Granter. The subsequent providers obtain user authentication information and password judgment are completely consistent. Specifically, the newly added authenticator in CaptchaTokenGranter and the authorizer in password mode
The difference of ResourceOwnerPasswordTokenGraner is that the getoauthauthentication () method of the former obtains the authentication information and adds the logic of verifying the verification code. The specific code implementation is explained in the actual battle.

2. Actual combat

The verification code authorization mode involves three parts: Spring Security OAuth2 extended verification code authorization mode, verification code generated in the background and verification code added by front-end login. It involves front-end and back-end things. You can select your own concerns according to your needs.

2.1 extension of authentication code authorization mode

It is known from the principle that only the ability of Granter to add verification code needs to be rewritten, so the authorizer who copies the password mode
ResourceOwnerPasswordTokenGranter is renamed CaptchaTokenGranter and becomes the authorizer of the verification code mode with minor changes.

CaptchaTokenGranter

Copy code 12345678910112131415161718192021222324252627282930333436373839404142434445464748495051525354555657585906162636465666768697071727374757677 JAVA/**
 * Verification code authorization mode authorizer
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class CaptchaTokenGranter extends AbstractTokenGranter {

    /**
     * Claim that the authorizer CaptchaTokenGranter supports the authorization mode captcha
     * Pass the value grant according to the interface_ The value of type = captcha matches this authorizer
     * See the following two methods for matching logic
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "captcha";
    private final AuthenticationManager authenticationManager;
    private StringRedisTemplate redisTemplate;

    public CaptchaTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
                               StringRedisTemplate redisTemplate
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        // Verification code verification logic
        String validateCode = parameters.get("validateCode");
        String uuid = parameters.get("uuid");

        Assert.isTrue(StrUtil.isNotBlank(validateCode), "Verification code cannot be empty");
        String validateCodeKey = AuthConstants.VALIDATE_CODE_PREFIX + uuid;
        
        // Extract the correct verification code from the cache and compare it with the verification code entered by the user
        String correctValidateCode = redisTemplate.opsForValue().get(validateCodeKey);
        if (!validateCode.equals(correctValidateCode)) {
            throw new BizException("Incorrect verification code");
        } else {
            redisTemplate.delete(validateCodeKey);
        }

        String username = parameters.get("username");
        String password = parameters.get("password");

        // Remove subsequent useless parameters
        parameters.remove("password");
        parameters.remove("validateCode");
        parameters.remove("uuid");

        // Same logic as password mode
        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
    }
}

Two changes have been made to the authorizer of password mode, which are summarized as follows:

  1. Modify grant_ Value of type   password   by   captcha;
  2. The getOAuth2Authentication() method adds verification code verification logic.

AuthorizationServerConfig

Rewrite TokenGranter in the AuthorizationServerConfig configuration class to support the newly added verification code mode authorizer CaptchaTokenGranter

Now, the Spring Security OAuth2 extended authentication code authorization is complete!!!

How's it going, Jane? It's not easy? I believe you may have doubts. Let's take a test first.

The client ID of the management front-end is mall admin web. Before testing, the client is given to support the verification code mode.

Enter the wrong verification code and the correct verification code in the login interface to see whether the effect can achieve the expected effect, and how to generate the verification code and how to transfer the value from the front end will be described later.

2.2 Spring WebFlux integration verification code Kaptcha

The function of verification code generation is to generate a random code, cache it in redis, and return the key ID (generally uuid) of redis and the picture of the random code to the front end. Because there is no business logic, it is directly placed in the gateway here. In addition to taking advantage of the performance advantages of WebFlux, it can also reduce one forwarding. The code structure diagram of Youlai gateway verification code is as follows:

CaptchaHandler

Copy code 123456789101121314151617181920212223242526272829303132 JAVA@Component
@RequiredArgsConstructor
public class CaptchaHandler implements HandlerFunction<ServerResponse> {

    private final Producer producer;
    private final StringRedisTemplate redisTemplate;

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest) {
        // Generate verification code
        String capText = producer.createText();
        String capStr = capText.substring(0, capText.lastIndexOf("@"));
        String code = capText.substring(capText.lastIndexOf("@") + 1);
        BufferedImage image = producer.createImage(capStr);
        // Cache verification code to Redis
        String uuid = IdUtil.simpleUUID();
        redisTemplate.opsForValue().set(AuthConstants.VALIDATE_CODE_PREFIX + uuid, code, 60, TimeUnit.SECONDS);
        // Conversion stream information write out
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", os);
        } catch (IOException e) {
            return Mono.error(e);
        }

        java.util.Map resultMap = new HashMap<String, String>();
        resultMap.put("uuid", uuid);
        resultMap.put("img", Base64.encode(os.toByteArray()));

        return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(Result.success(resultMap)));
    }
}

CaptchaConfig

The property kaptcha.textproducer.impl needs to specify the classpath of your own project text generator KaptchaTextCreator

Copy code 12 JAVA// Verification code text generator 
properties.setProperty("kaptcha.textproducer.impl", "com.youlai.gateway.kaptcha.KaptchaTextCreator");

CaptchaRouter

Copy code 12345678910 JAVA@Configuration
public class CaptchaRouter {

    @Bean
    public RouterFunction<ServerResponse> routeFunction(CaptchaHandler captchaHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/captcha")
                        .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), captchaHandler::handle);
    }
}

Verification code test

Modify the Nacos gateway configuration file youlai-gateway.yaml whitelist add request path / captcha

visit
http://localhost:9999/captcha As follows:

2.3 front end login access verification code mode

Login page

Add a verification code to the login form. The complete code address is: Mall admin web

src/views/login/index.vue

Copy code 12345678910112131415 HTML <el-form-item prop="validateCode">
    <span class="svg-container">
       <svg-icon icon-class="validCode"/>
     </span>
   <el-input
     v-model="loginForm.validateCode"
     auto-complete="off"
     placeholder="Please enter the verification code"
     style="width: 65%"
     @keyup.enter.native="handleLogin"
   />
   <div class="validate-code">
     <img :src="captchaUrl" @click="getValidateCode" height="38px"/>
   </div>
 </el-form-item>

The returned image is a Base64 encrypted string, so the prefix data: image / GIF is added; base64,

Copy code 12345678 JAVASCRIPT// Get verification code
getValidateCode() {
  getCaptcha().then(response => {
	const {img, uuid} = response.data
	this.captchaUrl = "data:image/gif;base64," + img
	this.loginForm.uuid = uuid;
  })
}

Interface request

src/store/modules/user.js set request parameters

Copy code 123456789101121314151617181920 JAVASCRIPTlogin({commit}, userInfo) {
  const {username, password, validateCode, uuid} = userInfo
  return new Promise((resolve, reject) => {
    login({  
      username: username,
      password: password,
      grant_type: 'captcha', // The authorization mode is specified as captcha verification code mode, which was originally password password mode
      uuid: uuid, // Obtain the identification of the correct verification code from Redis
      validateCode: validateCode // Verification Code
    }).then(response => {
      const {access_token, refresh_token, token_type} = response.data
      const token = token_type + " " + access_token
      commit('SET_TOKEN', token)
      setToken(token)
      setRefreshToken(refresh_token)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })

src/api/user.js set request header

Copy code 12345678910 JAVASCRIPTexport function login(params) {
  return request({
    url: '/youlai-auth/oauth/token',
    method: 'post',
    params: params,
    headers: {
      'Authorization': 'Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' // OAuth2 client information Base64 encryption, plaintext: Mall admin Web: 123456
    }
  })
}

3, Authorization mode of SMS verification code

1. Principle

The sequence diagram of SMS verification code mode is as follows. The changed roles are still marked with green background. You can see that the extension is the entry point for the authorizer Granter and the authentication Provider provider.

Authorization process of SMS verification code:   The process is basically consistent with the password mode, according to grant_ The type matches the authorizer, smscodeokengranter, and delegates it to the ProviderManager for authentication according to
Matching authentication provider for SmsCodeAuthenticationToken   SmsCodeAuthenticationProvider performs SMS verification code verification.

2. Actual combat

2.1 authorization mode extension of SMS verification code

SmsCodeTokenGranter

Copy code 123456789101121314151617181920212223242526272829303313233435363738394041424344454647484950515253545556 JAVA/**
 * Mobile phone verification code authorizer
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class SmsCodeTokenGranter extends AbstractTokenGranter {

    /**
     * SMS claims that the authorizer CaptchaTokenGranter supports the authorization mode_ code
     * Pass the value grant according to the interface_ type = sms_ The value of code matches this authorizer
     * See the following two methods for matching logic
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "sms_code";
    private final AuthenticationManager authenticationManager;

    public SmsCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        String mobile = parameters.get("mobile"); // cell-phone number
        String code = parameters.get("code"); // SMS verification code

        parameters.remove("code");

        Authentication userAuth = new SmsCodeAuthenticationToken(mobile, code);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + mobile);
        }
    }
}

SmsCodeAuthenticationProvider

Copy code 12345678910112131415161718192021222324252627282930331323343536373839 JAVA/**
 * SMS verification code authentication authorization provider
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private MemberFeignClient memberFeignClient;
    private StringRedisTemplate redisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCredentials();

        String codeKey = AuthConstants.SMS_CODE_PREFIX + mobile;
        String correctCode = redisTemplate.opsForValue().get(codeKey);
        // Verification code comparison
        if (StrUtil.isBlank(correctCode) || !code.equals(correctCode)) {
            throw new BizException("Incorrect verification code");
        } else {
            redisTemplate.delete(codeKey);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByMobile(mobile);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

AuthorizationServerConfig

In the authentication center configuration, add the SmsCodeTokenGranter to the set of authorization types of the authenticator.

2.2 Alibaba cloud free SMS application

visit
https://free.aliyun.com/product/cloudcommunication-free-trial?spm=5176.10695662.1128094.7.2a6b4bee30xtJx Apply for Alibaba cloud free SMS trial

Add signature and wait for approval

After the signature is approved, you can create an AccessKey access key

Add template, domestic message → template management → add template

After the signature is approved, the AccessKey and template CODE are obtained, and then the project integration can be carried out.

2.3 SpringBoot integrates alicloud SMS

There are many online tutorials on integrating SMS with SpringBoot. There is no icing on the cake here. Next, let's briefly talk about integrating Alibaba cloud SMS with Youlai mall. Complete source code

By convention, the SMS is encapsulated into a public module for reference to other application modules that need SMS.

Youlai auth introduces common SMS dependency

Copy code 123456 XML<dependencies> 
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>common-sms</artifactId>
    </dependency>
</dependencies>

The properties required by aliyunsmproperties need to be configured in the configuration center file youlai-auth.yaml of Nacos

Copy code 123456789 YAML# Alibaba cloud SMS configuration
aliyun:
  sms:
    accessKeyId: LTAI5tSxxxxxxNcD6diBJLyR
    accessKeySecret: SoOWRqpjtSxxxxxxM8QZ2PZiMTJOVC
    domain: dysmsapi.aliyuncs.com 
    regionId: cn-shanghai
    templateCode: SMS_225xxx770
    signName: Have come to technology

Send SMS verification code interface

Copy code 1234567891011213141516 JAVA@Api(tags = "SMS verification code")
@RestController
@RequestMapping("/sms-code")
@RequiredArgsConstructor
public class SmsCodeController {

    private final AliyunSmsService aliyunSmsService;

    @ApiOperation(value = "Send SMS verification code")
    @ApiImplicitParam(name = "phoneNumber", example = "17621590365", value = "cell-phone number", required = true)
    @PostMapping
    public Result sendSmsCode(String phoneNumber)  {
        boolean result = aliyunSmsService.sendSmsCode(phoneNumber);
        return Result.judge(result);
    }
}

2.4 mobile terminal access SMS verification code authorization mode

There is a front-end framework for mobile mall app to use uni app cross platform applications. Because the mall has always been presented as one end of wechat applet, the power of uni app can not be reflected. Take this opportunity to extend the authorization mode of SMS verification code for mall app and add the login interface of SMS verification code for H5, Android and IOS.

Let's take a look at the different rendering effects of the mall app login interface in H5/Android/IOS and wechat applets.

H5/Android/IOS login interface

Wechat applet login interface

The login page / pages/login/login.vue has different presentation on different platforms. The implementation principle is realized through #ifdef MP and #ifndef MP conditional compilation instructions, where #ifdef MP is compiled and effective on the applet platform, and #ifdef MP is compiled and effective on the non applet platform.

During development and compilation, when you click Run on the HBuilderX toolbar and select different platforms, different pages will be presented.

  1. Run → run to the built-in browser → SMS verification code login interface;
  2. Run → run to applet simulator → wechat developer tool → applet authorization login interface;

When it comes to accessing the SMS verification code of Spring Security OAuth2 extension, it is important to see how to transmit parameters. In the / api/user.js code of the mall app:

Copy code 123456789101121314151617 JAVASCRIPT// H5/Android/IOS SMS verification code login
// #ifndef MP
export function login( mobile,code) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			mobile: mobile,
			code: code,
			grant_type: 'sms_code'
		},
		headers: {
			'Authorization': 'Basic bWFsbC1hcHA6MTIzNDU2' // Client information Base64 encryption, plaintext: Mall app: 123456
		}
	})
}
// #endif

Give the mall app client support for sms_code mode

3. Test

At this point, the authorization mode for H5/Android/IOS mobile terminal to access the SMS verification code extended by Spring Security OAuth2 has been completed. The next extended authorization mode is for the authorized login of the mobile terminal of the hottest wechat applet.

4, Wechat authorization mode

1. Principle

The login authorization flow chart of wechat applet is as follows. Our role is the developer server. Our main work is to receive the code of the applet and obtain the openid and session from the wechat server_ After the key, the developer server generates a session (token) and returns it to the applet. The subsequent applet carries the token to interact with the developer server, so there is no wechat server.

The principle of Spring Security OAuth2 wechat authorization extension is the same as the above SMS verification code. Add the authorizer WechatTokenGranter to build WechatAuthenticationToken and match it to the authentication provider
WechatAuthenticationProvider completes the authentication authorization logic in its authenticate method.

2. Actual combat

2.1 wechat authorization mode extension

WechatTokenGranter

WechatTokenGranter wechat authorizer receives code, encryptedData and iv to build WechatAuthenticationToken

Copy code 123456789101121314151617181920212223242526272829303313233435363738394041424344454647484950515253 JAVA/**
 *  Wechat authorizer
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class WechatTokenGranter extends AbstractTokenGranter {

    /**
     * wechat authorizer CaptchaTokenGranter supports authorization mode
     * Pass the value grant according to the interface_ The value of type = wechat matches this authorizer
     * See the following two methods for matching logic
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "wechat";
    private final AuthenticationManager authenticationManager;

    public WechatTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String code = parameters.get("code");
        String encryptedData = parameters.get("encryptedData");
        String iv = parameters.get("iv");

        parameters.remove("code");
        parameters.remove("encryptedData");
        parameters.remove("iv");

        Authentication userAuth = new WechatAuthenticationToken(code, encryptedData,iv); // Unauthenticated status
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth); // Under certification
        } catch (Exception e) {
            throw new InvalidGrantException(e.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) { // Authentication successful
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else { // Authentication failed
            throw new InvalidGrantException("Could not authenticate code: " + code);
        }
    }
}

WechatAuthenticationProvider

Finally, the authentication logic is completed in the authenticate() method of the wechat authentication provider, and the token is returned successfully.

Copy code 12345678910112131415161718192021222324252627282930331323343536373839404142434445464748495051525354555657585960 JAVA/**
 * Wechat authentication provider
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class WechatAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private WxMaService wxMaService;
    private MemberFeignClient memberFeignClient;

    /**
     * Wechat authentication
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        WechatAuthenticationToken authenticationToken = (WechatAuthenticationToken) authentication;
        String code = (String) authenticationToken.getPrincipal();

        WxMaJscode2SessionResult sessionInfo = null;
        try {
            sessionInfo = wxMaService.getUserService().getSessionInfo(code);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        String openid = sessionInfo.getOpenid();
        Result<MemberAuthDTO> memberAuthResult = memberFeignClient.loadUserByOpenId(openid);
        // Wechat user does not exist, register as a new member
        if (memberAuthResult != null && ResultCode.USER_NOT_EXIST.getCode().equals(memberAuthResult.getCode())) {

            String sessionKey = sessionInfo.getSessionKey();
            String encryptedData = authenticationToken.getEncryptedData();
            String iv = authenticationToken.getIv();
            // Decrypt encryptedData to obtain user information
            WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, iv);

            UmsMember member = new UmsMember();
            BeanUtil.copyProperties(userInfo, member);
            member.setOpenid(openid);
            member.setStatus(GlobalConstants.STATUS_YES);
            memberFeignClient.add(member);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByOpenId(openid);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return WechatAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

2.2 wechat applet access wechat authorization mode

Also in the interface file of the mall app, / api/user.js, let's first look at how the applet side transfers values?

Copy code 12345678910112131415161718 JAVASCRIPT// Applet authorized login
// #ifdef MP
export function login(code, encryptedData,iv) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			code: code,
			encryptedData: encryptedData,
			iv:iv,
			grant_type: 'wechat'
		},
		headers: {
			'Authorization': 'Basic bWFsbC13ZWFwcDoxMjM0NTY=' // Client information Base64 encryption, plaintext: Mall Web: 123456
		}
	})
}
// #endif

Set the OAuth2 client to support wechat authorization mode

3. Test

At this point, the wechat authorization extension has been completed, and the three authorization modes commonly used in the actual business scenario have come to an end.

However, if you know something about Spring Security OAuth2, you may wonder whether the refresh mode corresponding to these extended modes needs to be adjusted?

If the extension is only for a user system and an authentication method (user name / mobile number / openid), such as the extension of verification code mode, there is no need to adjust the refresh mode.

However, if it is a multi-user system or multiple authentication methods, Youlai mall is a multi-user system and multiple authentication methods. At this time, you must make some adjustments to adapt, but the changes are not big. Why and how to adjust are described in detail below.

5, Multi user system refresh mode

1. Principle

The timing chart of the refresh mode is as follows. Compared with the password mode, it is only the change of Granter and Provider.

Focus on the authentication provider for refresh mode
PreAuthenticatedAuthenticationProvider. Its authenticate() authentication method only performs user status verification, and the check() method is called
AccountStatusUserDetailsChecker#check(UserDetails).

Pay attention
this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken)authentication); of   Preauthenticateduserdetailsservice user service.

When the authorization mode is not extended, it is set as follows

Then in
Authorization server endpoints configurer #adduserdetailsservice (defaulttoken services, userdetailsservice) construct   UserDetailService user service is set in PreAuthenticatedAuthenticationProvider.

In this way, the problem can be imagined under multi-user system authentication. Users include system users and member users respectively. It is certainly impossible to fix a user service here. When creating a Provider in the extended authorization mode, you can specify a specific user service UserDetailService, as follows:

You can add a corresponding refresh mode for each authorization mode extension, but it is troublesome. The core diagram of the implementation scheme in this paper is simple and effective, so the other scheme used here is to reset
Of PreAuthenticatedAuthenticationProvider   The preAuthenticatedUserDetailsService property enables it to judge and select the user system and authentication method.

2. Actual combat

Firstly, we know that an OAuth2 client basically corresponds to a user system. For example, the corresponding relationship between the client and the user system of Youlai mall project is as follows:

OAuth2 client name

OAuth2 client ID

User system

management system

mall-admin-web

System user

H5/Android/IOS mobile terminal

mall-app

Mall member

Applet side

mall-weapp

Mall member

There is a very simple and effective idea. You can maintain a mapping relationship Map in the system as shown in the above table, and then select the user system according to the transmitted client ID.

That's it? Of course not. There is another point you must consider. For example, although the user system of the mobile terminal is a member user, it may have a variety of authentication methods, such as SMS authentication code, user name and password, and even more authentication methods.

The default UserDetailsService interface of Spring Security OAuth2 has only one loadUserByUsername() method, which obviously cannot support multiple authentication methods.

Copy code 123 JAVApublic interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

Therefore, you need to add an authentication method in the implementation class of UserDetailsService, and then convert UserDetailsService into a specific implementation class at runtime. For details, see the following items
The implementation of MemberUserDetailsServiceImpl supports both mobile phone number and three-party ID openid to obtain user authentication information, that is, two different authentication methods.

Copy code 12345678910112131415161718192021222324252627282930333435373839404142434445464748495051525354555657575859061626364656667686970717273 JAVA/**
 * Mall member user authentication service
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 */
@Service("memberUserDetailsService")
@RequiredArgsConstructor
public class MemberUserDetailsServiceImpl implements UserDetailsService {

    private final MemberFeignClient memberFeignClient;

    @Override
    public UserDetails loadUserByUsername(String username) {
        return null;
    }

    /**
     * Mobile phone number authentication method
     *
     * @param mobile
     * @return
     */
    public UserDetails loadUserByMobile(String mobile) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.MOBILE.getValue());   // Authentication method: OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("The account has been disabled!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("The account has been locked!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("This account has expired!");
        }
        return userDetails;
    }


    /**
     * openid Authentication mode
     *
     * @param openId
     * @return
     */
    public UserDetails loadUserByOpenId(String openId) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByOpenId(openId);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.OPENID.getValue());   // Authentication method: OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("The account has been disabled!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("The account has been locked!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("This account has expired!");
        }
        return userDetails;
    }
}

New
PreAuthenticatedUserDetailsService can select UserDetailService and method to obtain user information UserDetail according to the client and authentication method

Copy code 12345678910112131415161718192021222324252627282930331323353637383940414243444546474849505152535455565757585906162636465666768 JAVA/**
 * Refresh the token and authenticate UserDetailsService again
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/10/2
 */
@NoArgsConstructor
public class PreAuthenticatedUserDetailsService<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {

    /**
     * Mapping of client ID and user service UserDetailService
     *
     * @see com.youlai.auth.security.config.AuthorizationServerConfig#tokenServices(AuthorizationServerEndpointsConfigurer)
     */
    private Map<String, UserDetailsService> userDetailsServiceMap;

    public PreAuthenticatedUserDetailsService(Map<String, UserDetailsService> userDetailsServiceMap) {
        Assert.notNull(userDetailsServiceMap, "userDetailsService cannot be null.");
        this.userDetailsServiceMap = userDetailsServiceMap;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsServiceMap, "UserDetailsService must be set");
    }

    /**
     * Override the preAuthenticatedUserDetailsService property of PreAuthenticatedAuthenticationProvider. You can select user service UserDetailService to obtain user information UserDetail according to the client and authentication method
     *
     * @param authentication
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
        String clientId = RequestUtils.getOAuth2ClientId();
        // Obtain the authentication method. The default is username
        AuthenticationMethodEnum authenticationMethodEnum = AuthenticationMethodEnum.getByValue(RequestUtils.getAuthenticationMethod());
        UserDetailsService userDetailsService = userDetailsServiceMap.get(clientId);
        if (clientId.equals(SecurityConstants.APP_CLIENT_ID)) {
            // The user system of the mobile terminal is a member, and the authentication method is mobile authentication through the mobile number
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case MOBILE:
                    return memberUserDetailsService.loadUserByMobile(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.WEAPP_CLIENT_ID)) {
            // The user system of the applet is a member, and the authentication method is openid authentication through the wechat three-party logo
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case OPENID:
                    return memberUserDetailsService.loadUserByOpenId(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.ADMIN_CLIENT_ID)) {
            // The user system of the management system is the system user, and the authentication method is authenticated by the user name username
            switch (authenticationMethodEnum) {
                default:
                    return userDetailsService.loadUserByUsername(authentication.getName());
            }
        } else {
            return userDetailsService.loadUserByUsername(authentication.getName());
        }
    }
}

AuthorizationServerConfig configuration reset
Of PreAuthenticatedAuthenticationProvider   preAuthenticatedUserDetailsService property value

Copy code 123456789101121314151617181920212223242526272829303313233536373839404142434445464748495051525354555657585906162636465666768697071 JAVA    /**
     * Configure authorization, access endpoint of token and token services
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token enhancement
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        // Obtain the authorizer of the original default authorization mode (authorization code mode, password mode, client mode and simplified mode)
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));

        // Add verification code authorization mode authorizer
        granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager, stringRedisTemplate
        ));

        // Add the authorizer of SMS verification code authorization mode
        granterList.add(new SmsCodeTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        // Add the authorizer of wechat authorization mode
        granterList.add(new WechatTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
                /** refresh token There are two usage methods: reuse (true) and non reuse (false). The default is true
                 *  1 Reuse: when the access token is expired and refreshed, the expiration time of the refresh token remains unchanged, and the time of initial generation still prevails
                 *  2 Non reuse: when the access token expires and refreshes, the refresh token expires. Within the validity period of the refresh token, the refresh will never expire, so that there is no need to log in again
                 */
                .reuseRefreshTokens(true)
                .tokenServices(tokenServices(endpoints))
        ;
    }


    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // In the multi-user system, refresh the token to re authenticate the mapping Map of the client ID and UserDetailService
        Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
        clientUserDetailsServiceMap.put(SecurityConstants.ADMIN_CLIENT_ID, sysUserDetailsService); // Management system client
        clientUserDetailsServiceMap.put(SecurityConstants.APP_CLIENT_ID, memberUserDetailsService); // Android/IOS/H5 mobile client
        clientUserDetailsServiceMap.put(SecurityConstants.WEAPP_CLIENT_ID, memberUserDetailsService); // Wechat applet client

        // Reset PreAuthenticatedAuthenticationProvider#preAuthenticatedUserDetailsService to distinguish user systems and obtain authenticated user information according to client ID and authentication method
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
        return tokenServices;
    }

The core code is basically above. After the above adjustments are completed, the refresh mode is OK. Next, test the refresh mode corresponding to the newly extended authorization mode one by one.

3. Test

3.1 operation instructions for importing cURL from postman

All the following tests will post cURL. Why do you emphasize this? Originally, I thought that I put the complete request screenshot of obtaining the token by testing Spring Security OAuth2 with Postman into the project description document README.md, so that no one would ask the login interface 403 to report an error, but the fact feedback was really disappointing, so that I basically chose to be silent when there were such questions later. I hope you can think and understand in a transposition. Therefore, the idea this time is to post the interface information in the form of cURL, and then directly import it into Postman test.

The following is the URL of the item to get the token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

Enter Postman and select File → Import → Raw text to Import the above cURL

3.2 password mode test

Client information used for password mode test, client ID: client key: Mall admin Web: 123456 ----- Base64 online coding →
bWFsbC1hZG1pbi13ZWI6MTIzNDU2

If you want to change the client, please change the client information in the request header Authorization of the interface below, otherwise you will be prompted with 403 because your client information is incorrect and authentication is unsuccessful. Access is prohibited.

Some people will ask that there are projects that do not customize the handling of client authentication exceptions. In fact, I provided solutions in my previous articles
https://www.cnblogs.com/haoxianrui/p/14028366.html#3 -The client authentication is abnormal. If necessary, it can be adjusted according to the article. As for why there is no solution in the project, first of all, I think the implementation is more complex. If you have a good solution, you are welcome to put forward it. In addition, this kind of client information error can be avoided as a developer.

Get token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

Refresh token

refresh_ The token needs to be replaced. In the first step, get the refresh returned by the token_ token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJmYzdiOGNhZi1iNmI4LTRlZTEtOGE4OC0yYzdmZTcxNTA0YjEiLCJleHAiOjE2MzQ0NDg5NDIsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiOGU3ZWE5MjAtOGQ0Ni00NmFlLWI3ODYtZTc3ZjAxY2Y5ZjIyIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.I_9uLpr7WUeb-JNSBr17Ya59qP3a8EFSps3MwqpTS-mlDldx-HDsJM41Pl11-b_99_yhl_h-FRhIYpGaOqP4p7428z_LQmlpBrebx9TVcSk_gVbDPjN3Q2glxaupvCGmAuRNWby0Aam-On2wO8RkKKhH0arI2nf4rseu18WN0-cqxJuYn10hyQ-T7n5n3zjnx92nMyqESWqfPqsy8_eie-can4113PBHhnqs9QI1SQ-1Z_AtZLgAb1FzaV2JuTqqbPlVULM-uaQnIoe0zNq5R-TYoUJ2cQNkP4YOR4e9TP26iSPLNlcsg59TFHi0UhrZiZqvS3i5nUkqV0jpzvYVrg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' 

3.3 verification code mode test

The verification code mode test uses the client information, client ID: client key: Mall admin Web: 123456 ----- Base64 online coding →
bWFsbC1hZG1pbi13ZWI6MTIzNDU2

Get token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=captcha&uuid=11add22b38e74a57bade0bf628a70645&validateCode=1' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2

Refresh token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJiMTU5ZGU2Ni1iYmY5LTRmOWEtYTg1MC1kMjk1MDJiYTNjY2IiLCJleHAiOjE2MzQ0NjQxNjUsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiN2MwNDk2YzgtMTRjMC00MWJhLTk2OTUtYTk2ZGYwODQ1NGMxIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.j3n1FrMEIRkb_-3YhoDdPA4qBofzjD4y6HWdhCRdIjWU3D1La9ee_guhdeEEL49sfdHQSek_T4funyUCegTCdxfowzh3JghtCXFyRdxSWxjgJalgSIGVcOSEePxADwf2biHB3m6WzpOT9FxEdBavT7mfdQRjfc276uL7zzi5blKc4pUzX9l1AvReMP7azT_6soBNi-nid5maUCpMx_w9AVUvjVl4L7QMCO22zEogs2SlpMpggAITMv3QKYYTZ3vzxL2oNR_r-9qXqN7W6DxGqQc1gIqXADX1oqsXzD4AaAtLqOslP8FM6HiOzzZVd1kmv1cPHzVzabx6vYUZFA1PMg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

3.4 SMS verification code test

Information of the client used for SMS verification code mode test, client ID: client key: Mall app: 123456 ----- Base64 online coding →
bWFsbC1hZG1pbi13ZWI6MTIzNDU2

Get token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?mobile=17621590365&code=666666&grant_type=sms_code' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

Refresh token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGlvbk1ldGhvZCI6Im1vYmlsZSIsInVzZXJfbmFtZSI6IjE3NjIxNTkwMzY1Iiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBlZGMyZjI0LWFiNWUtNDkxYy1iYjAyLTdlOWJkN2U5M2Y0MiIsImV4cCI6MTYzNDQ2NTEzMCwidXNlcklkIjo1OSwianRpIjoiZjcyMWZhZjAtZTczMS00MmUxLTgxYjAtMjg4NDEwZjQzODA0IiwiY2xpZW50X2lkIjoibWFsbC1hcHAiLCJ1c2VybmFtZSI6IjE3NjIxNTkwMzY1In0.RdtJiNhk3OheoUcpUtM9JBgwLfSt1k3FhEvgMYeDSFwf28TeS_SF2LY7vzOrbJfYQZuaMzvMfoSljeDuQoBr38Ebh2LogbZClaDY72TO9P88DAW-1l2Rjm1XYFMEzCZYweDehT2tJU6eOwN8GZ40dzcCnqjZwgCKgoIdJksxMB6n96Kfmxw_Z3TUny5j2mdDZB79bwWci86jev6y-RUTjbZWRu1vH4MVJ0hCOCRARoem1jlkW6nnkzhE84OasDI9RCg5jsA_ZNs3x-rFNnRY7T5gQOAOwPvJKVcXww35BGYZGHCHqQb6QEbxul6Pg1rLjFU6YgsSO1Xq_cWVOt0Nvg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

3.5 wechat authorization mode test

Information of the client used in wechat authorization mode test, client ID: client key: Mall Web: 123456 ----- Base64 online coding → bWFsbC13ZWFwcDoxMjM0NTY=

Get token

Copy code 12 SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?code=063hEOFa1N1dWB0XpRIa1WvNw74hEOF-&encryptedData=1qmFeCKbTxZyCdzctu37sX+jOnM9dZG9lKyD3v6FhA5sCEtDwaF/wqyVR70QVrqt7bGVH+Kb+PBsFJlBXUdjnFGlrwmPqgNusI4f5eA8SvZgopvmlzJhXwe+OjLCQooeGnSkcnUrUuMA/G4ZYWFeljaHhxJq/75APWs4HyLANfbeLp50qI9xrRJVUXlTqdqJ0ub38ZxWVvWZMqY8FaskAiZpxzrF30eXu93BCpDavRCVzlSfv6LFJmmvEGVOKr4Wap9ND82N3sDMyArRsdhdhmoWIYBbRs+iLbKcS4WyOhpmaQr4fhhOuxO+zSAa7W+eNmCH2Id6Pgpvhl6ureNNzEb0cQLoksP6oakPmv/yEiw5fnW6Oi9jJbxzlMyORN3/atHgBl6zLIgS9UMhFE+42Vp5B3L8jLly4+B4NpNgol+khXoh+ycUXSRPV4bUuriv&iv=j+brWSrqRW+d4lAjRWW4RA==&grant_type=wechat' \
--header 'Authorization: Basic bWFsbC13ZWFwcDoxMjM0NTY='

Refresh token

6, Summary

Based on Spring Security OAuth2, this paper extends the commonly used verification code mode, SMS verification code mode and wechat authorization mode, and applies them to the management front end, mobile application end and wechat applet end of Youlai mall respectively. At the same time, slightly adjust the refresh mode to adapt to several extended modes and multi-user system. Through the extension of authorization mode, the authentication process and underlying principle of Spring Security OAuth2 are exposed. It is believed that after having a clear idea of the process and principle, different authentication requirements can be handy. Finally, I still sigh the charm of the Spring framework, that is, you can feel that it will leave you an extension entrance on the basis of function implementation, rather than make you want to change its source code to implement. Finally, I hope everyone can gain something. Although we don't want to do anything here, and it's not good for us to write these to tell the truth, after all, it took more than half a month to write this article. It's my own effort, and I don't want it to be wasted

Tags: Java Programming Spring Back-end Programmer

Posted on Mon, 25 Oct 2021 07:57:39 -0400 by zz50