Wechat authorization is the principle. Spring Cloud OAuth2 authorization code mode

Last article Spring Cloud OAuth2 single sign on This paper introduces the use of password mode for identity authentication and single sign on. This article introduces another authorization mode of Spring Cloud OAuth2 - authorization code mode.

The authentication process of authorization code mode is as follows:

1. The user client requests the authentication interface of the authentication server, and attach the callback address;

2, the authentication service interface is adjusted to its login interface after receiving the authentication request.

3. The user enters the user name and password, clicks confirm, and jumps to the authorization and rejection prompt page (it can also be omitted);

4. After clicking authorization or default authorization, the user will jump to the callback address of the microservice client and pass in the parameter code;

5. The callback address is generally a RESTful interface. After the interface obtains the code parameter, it requests the token of the authentication server to obtain the interface again in exchange for access_token and other information;

6. After obtaining the access_token, take the token to request the interface of each microservice client.

Note that the user client mentioned above can be understood as browser and app. The microservice client is the microservice in our system, such as order service and user service. The authentication server is used for authentication and authorization. Compared with the authentication server, each business microservice can also be called its client.

Authentication server configuration

The authentication server continues to use the configuration of the previous article. The code does not need to be changed. It only needs to add a record in the database to support the authentication of the new microservice client

The client ID of the client we want to create is code client, and the client secret is code-secret-8888. However, encryption is also required, which can be obtained by the following code:

System.out.println(new BCryptPasswordEncoder().encode("code-secret-8888"));

In addition to the above two parameters, you need to set authorized \ grant \ types to authorization \ code, refresh \ token, and web \ server \ redirect \ URI to the callback address. Later, the microservice client will create this interface.

Then organize the record and insert it into the database.

INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('code-client', '$2a$10$jENDQZRtqqdr6sXGQK.L0OBADGIpyhtaRfaRDTeLKI76I/Ir1FDn6', 'all',
    'authorization_code,refresh_token', 'http://localhost:6102/client-authcode/login', null, 3600, 36000, null, true);

Create microservices of authorization mode

Introducing maven package

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

okhttp and thmeleaf are introduced because they need to make a simple page and simulate the normal authentication process.

Configuration file application.yml

spring:
  application:
    name: client-authcode
server:
  port: 6102
  servlet:
    context-path: /client-authcode


security:
  oauth2:
    client:
      client-id: code-client
      client-secret: code-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:6001/oauth/token_key
        key-value: dev
    authorization:
      check-token-access: http://localhost:6001/oauth/check_token

Create resourceConfig

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

        accessTokenConverter.setSigningKey("dev");
        accessTokenConverter.setVerifierKey("dev");
        return accessTokenConverter;
    }

    @Autowired
    private TokenStore jwtTokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(jwtTokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/login").permitAll();
    }
}

Use jwt as the token storage. Note that the / login interface is allowed to access without authorization. This address is the callback address of the authentication, and the code parameter will be returned.

Create application.java startup class

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

You can stop at this step. We start the authentication server and the newly created authentication client, and then we can test it manually. The callback interface hasn't been created yet. That's OK. We have the right to use that address to receive code parameters.
1. Access the / oauth/authorize authorization interface in the browser. The interface address is:

http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login 

Note that the response "type parameter is set to code and the redirect" URI is set to the callback address inserted in the database.

2. After entering the above address, it will automatically jump to the login page of the authentication server and enter the user name and password, where the user name is admin and the password is 123456

3. Click OK to go to the authorization confirmation page, where there are two buttons: Authorize and Deny. You can cancel the display of this page by setting the autoapprove field to 0. By default, you agree to Authorize directly.

4. After clicking agree to authorize, we jump to the callback address. Although it is 404, we just want to get the code parameter. Pay attention to the code parameter after the address.

5. This code parameter is obtained to request access_token from the authentication server / oauth/token interface, and continue to send the request with REST Client. Similarly, you can test with postman and other tools.

Note that the grant type parameter is set to authorization code. Code is added in the callback address in the previous step. The redirect URI still needs to be brought as the verification condition. If it is not brought or inconsistent with the previous setting, an error will occur.

The Authorization of the request header is still Basic + space + Base64 (client ﹣ ID: client ﹣ secret). Base64 can be encoded online through https://www.sojson.com/base64.html.

code-client:code-secret-8888 after base64 encoding, the result is y29kzs1jbgllbnq6y29kzs1zwnyzxqtodg4oa==

POST http://localhost:6001/oauth/token?grant_type=authorization_code&client=code-client&code=BbCE34&redirect_uri=http://localhost:6102/client-authcode/login
Accept: */*
Cache-Control: no-cache
Authorization: Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==

After sending the request, the returned json content is as follows:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MjYwMTMzMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2OWRmY2M4Yy1iZmZiLTRiNDItYTZhZi1hN2IzZWUyZjI1ZTMiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.WlgGnBkNdg2PwKqjbZWo6QmUmq0QluZLgIWJXaZahSU",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjY5ZGZjYzhjLWJmZmItNGI0Mi1hNmFmLWE3YjNlZTJmMjVlMyIsImV4cCI6MTU3MjYzMzczMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkNzk2OWRhMS04NTg4LTQ2YzMtYjdlNS1jMGM5NzcxNTM5Y2YiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.TEz0pQOhST9-ozdoJWm6cf1SoWvPC6W-5JW9yjZJXek",
  "expires_in": 3599,
  "scope": "all",
  "jwt-ext": "JWT Extended information",
  "jti": "69dfcc8c-bffb-4b42-a6af-a7b3ee2f25e3"
}

It is consistent with the token content obtained from the password mode in the previous article, and the next requests need to be accompanied by the access_token.

6. By substituting the access token obtained into the position of ${access token} in the following request, you can request the interface in the microservice that needs to be authorized to access.

GET http://localhost:6102/client-authcode/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ${access_token}

The interface contents are as follows:

@org.springframework.web.bind.annotation.ResponseBody
@GetMapping(value = "get")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object get(Authentication authentication)
{
    //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    authentication.getCredentials();
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
    String token = details.getTokenValue();
    return token;
}

Through the above manual test, it is proved that this process is feasible, but it has not yet reached automation. If you have integrated wechat login, you must know what we have done in the callback address. Take the returned code parameter to the token interface to exchange for access_token. Yes, the idea is the same. In our callback interface, we also need to exchange code for access_token.

To do this, I made a simple page and requested the interface to get the token in the callback interface.

Create a simple login page

Create the templates directory under the resources directory to store the template of thymeleaf, do not make the style, only make the simplest demonstration, and create the index.html template. The content is as follows:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Ancient kites-OAuth2 Client</title>
</head>
<body>
<div>
    <a href="http://Localhost: 6001 / OAuth / authorize? Client? Id = code client & response? Type = code & redirect? URI = http: / / localhost: 6102 / client authcode / login "> login</a>
    <span th:text="'Current authenticated user:' + ${username}"></span>
    <span th:text="${accessToken}"></span>
</div>
</body>
</html>

Callback interface and other interfaces

@Slf4j
@Controller
public class CodeClientController {

    /**
     * Used to display the index.html template
     * @return
     */     
    @GetMapping(value = "index")
    public String index(){
        return "index";
    }

    @GetMapping(value = "login")
    public Object login(String code,Model model) {
        String tokenUrl = "http://localhost:6001/oauth/token";
        OkHttpClient httpClient = new OkHttpClient();
        RequestBody body = new FormBody.Builder()
                .add("grant_type", "authorization_code")
                .add("client", "code-client")
                .add("redirect_uri","http://localhost:6102/client-authcode/login")
                .add("code", code)
                .build();

        Request request = new Request.Builder()
                .url(tokenUrl)
                .post(body)
                .addHeader("Authorization", "Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==")
                .build();
        try {
            Response response = httpClient.newCall(request).execute();
            String result = response.body().string();
            ObjectMapper objectMapper = new ObjectMapper();
            Map tokenMap = objectMapper.readValue(result,Map.class);
            String accessToken = tokenMap.get("access_token").toString();
            Claims claims = Jwts.parser()
                    .setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(accessToken)
                    .getBody();
            String userName = claims.get("user_name").toString();
            model.addAttribute("username", userName);
            model.addAttribute("accessToken", result);
            return "index";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    @org.springframework.web.bind.annotation.ResponseBody
    @GetMapping(value = "get")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Object get(Authentication authentication) {
        //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        authentication.getCredentials();
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        String token = details.getTokenValue();
        return token;
    }
}

The index() method is to show the thymeleaf template, and the login method is the callback interface. Here, okhttp3 is used as the interface request, and the / oauth/token interface of the request authentication server is used in exchange for the access_token, which only automates the steps of our manual test.

Visit the index.html page

Let's assume that this page is the homepage of a website. Users who are not logged in will see the login button on the website. Let's visit this page: http://localhost:6102/client-authcode/index , see the page like this

Next, click the login button. From the above template code, you can see that after clicking it, you will actually jump to the address you visited in the first step of our manual test. The subsequent operations are the same as those manually tested above. Enter the user name and password, and click agree to authorize.

Next, the page jumps back to call back the address < http: / / localhost: 6102 / client authcode / login? Code = XXX. The login method takes the code parameter, starts to construct the post request body, adds Authorization to the request header, and then requests the oauth/token interface. Finally, the obtained token and the username resolved through the token are returned to the front end. The final effect is as follows: Next:

Finally, after getting the token, the client can add the token to the request header to access the interface to be authorized.

Combined with the previous article, we have implemented oauth2 authentication of password and authorization code.

The source code address of the source microservice client of this chapter is: Click to view github source code

Related reading

Spring Cloud OAuth2 single sign on

Don't grudge your "recommendation"

Welcome to update this series and other articles from time to time
Ancient kites entered public numbers and joined the exchange group.

Tags: Java Spring Thymeleaf Database OkHttp

Posted on Wed, 06 Nov 2019 18:46:24 -0800 by darksniperx