Oauth2 finally understand

basic knowledge

What is Oauth

Simply put, OAuth is an authorization mechanism. The owner of the data tells the system that they agree to authorize third-party applications to enter the system and obtain these data. The system thus generates a short-term entry token (token), which is used in place of a password for use by third-party applications. The core of OAuth is to issue tokens to third-party applications. Oauth2 is the second version of Oauth. For a simple understanding, please refer to the article http://www.ruanyifeng.com/blog/2019/04/oauth_design.html

Explanation of related terms

  • Third-party application (Third-party application): Also known as client (client), for example, our product wants to use QQ, WeChat and other third parties to log in. For our products, QQ and WeChat login are third-party login systems. We also need third-party login system resources (avatars, nicknames, etc.). We are third-party applications for QQ, WeChat and other systems.
  • HTTP service provider (HTTP service): Our cloud note products, QQ, WeChat, etc. can all be called "service providers".
  • Resource Owner: Also known as user.
  • User Agent (User Agent): For example, browsers, instead of users to access these resources.
  • Authentication server (Authorization server): It is a server specially used by the service provider to handle authentication. Simply put, it is the login function (verify whether the user's account password is correct and assign the corresponding authority)
  • Resource server: The server where the service provider stores user-generated resources. It can be the same server as the authentication server, or it can be a different server. To put it simply, it is the access portal for resources.

Interactive process

oAuth sets up an authorization layer between the "client" and the "service provider". The "client" cannot directly log in to the "service provider", but can only log in to the authorization layer to distinguish the user from the client. The token used by the "client" to log in to the authorization layer is different from the user's password. The user can specify the scope and validity period of the authorization layer token when logging in. After the "client" logs in to the authorization layer, the "service provider" will open the user's stored data to the "client" according to the scope and validity of the token.
Insert picture description here

Token access and refresh

Access Token

Access Token is the token for the client to access the resource server. Owning this token means getting the user's authorization. However, this authorization should be temporary and have a certain validity period. This is because the Access Token may be leaked during use. Limiting a shorter validity period to Access Token can reduce the risk of Access Token leakage.

However, after the validity period is introduced, the client is not so convenient to use. Whenever the Access Token expires, the client must request authorization from the user again. In this way, users may need to perform authorization operations every few days or even every day. This is a matter that greatly affects the user experience. Hope there is a way to avoid this situation.

So oAuth2.0 introduced the Refresh Token mechanism

Refresh Token

The function of Refresh Token is to refresh Access Token. The authentication server provides a refresh interface, for example:

http://www.funtl.com/refresh?refresh_token=&client_id=

Pass in refresh_token and client_id, and after the authentication server passes the verification, a new Access Token will be returned. For security, oAuth2.0 introduces two measures:

  • oAuth2.0 requires that the Refresh Token must be stored on the client's server, and must not be stored on the narrowly defined client (such as App, PC software). When the refresh interface is called, it must be the access from the server to the server.
  • oAuth2.0 introduced the client_secret mechanism. That is, each client_id corresponds to a client_secret. This client_secret will be assigned to the client along with the client_id when the client applies for the client_id. The client must keep the client_secret on the server properly and must not disclose it. When refreshing the Access Token, the client_secret needs to be verified.

The actual refresh interface is similar to:

http://www.funtl.com/refresh?refresh_token=&client_id=&client_secret=

The above is the Refresh Token mechanism. The Refresh Token has a very long validity period. When the user is authorized, it will be redirected to the callback URL along with the Access Token and passed to the client.

Client authorization mode

The client must be authorized by the user (authorization grant) in order to obtain the token (access token). oAuth 2.0 defines four authorization methods.

  • implicit: simplified mode, not recommended
  • authorization code: authorization code mode
  • resource owner password credentials: password mode
  • client credentials: client mode
Simplified mode

The simplified mode is suitable for purely static page applications. The so-called purely static page application means that the application does not have the authority to execute code on the server (usually the code is hosted on someone else's server), and only has the control right of the front-end JS code.

In this scenario, the application does not have the ability to persist storage. Therefore, according to the provisions of oAuth2.0, this kind of application cannot get the Refresh Token. The entire authorization process is as follows: In
Insert picture description here
this mode, access_token is easy to leak and cannot be refreshed

Authorization code mode

The authorization code mode is suitable for applications that have their own server. It is a one-time temporary credential used in exchange for access_token and refresh_token. The authentication server provides an interface similar to this:

https://www.funtl.com/exchange?code=&client_id=&client_secret=

Need to pass in code, client_id and client_secret. After the verification is passed, access_token and refresh_token are returned. Once the exchange is successful, the code becomes invalid immediately and cannot be used a second time. The flow chart is as follows:
Insert picture description here
The function of this code is to protect the security of the token. As mentioned in the previous section, tokens are not safe in simple mode. This is because the token is directly returned to the application in step 4. And this step is easy to be intercepted and eavesdropped. After the code is introduced, even if the attacker can steal the code, he cannot obtain the client_secret stored in the server by the application, so he cannot exchange the code for the token. And at step 5, why is it not easy to be intercepted and eavesdropped? This is because, firstly, this is an access from the server to the server, which is more difficult for hackers to catch; secondly, this request usually requires the implementation of https. Even if you can eavesdrop on the data packet, you cannot parse out the content.

With this code, the security of the token is greatly improved. Therefore, oAuth2.0 encourages the use of this method for authorization, while the simple mode is only used when there is a last resort.

Password mode

In the password mode, the user provides his user name and password to the client. The client uses this information to ask for authorization from the "service provider". In this mode, the user must give his password to the client, but the client must not store the password. This is usually used when the user has a high degree of trust in the client, such as the client is part of the operating system.

A typical example is that different products within the same company use the company's oAuth2.0 system. In some cases, the product hopes to be able to customize the authorization page. Since it is the same enterprise, it is not necessary to show the words "xxx will obtain the following permissions" to the user and ask the user's authorization intention, but only need to perform the user's identity authentication. At this time, the specific product team develops a customized authorization interface, accepts the user's input account password, and directly passes it to the authentication server for authorization. This mode can be used for separate logins from the front and back ends.
Insert picture description here
One thing that needs special attention is that in step 2, the authentication server needs to verify the identity of the client to ensure that it is a trusted client.

Client mode

If the trust relationship goes further, or the caller is a back-end module and there is no user interface, the client mode can be used. The authentication server directly authenticates the client, and returns the token after passing the verification.
Insert picture description here

HelloWord

Here takes the authorization mode as an example to build a complete demo.
The following services are mainly included in this demo:

  • third-party usage
  • Authorization server
  • Resource server
  • user

The table values ​​are organized as follows:

project port Remarks
auth-server 8080 Authorization server
user-server 8081 Resource server
client-app 8082 third-party usage

In the case of our common authorization mode login, the various roles involved are provided and tested by themselves, so that you can understand the working principle of Oauth2 to the greatest extent.

Before building each service, first create a Maven parent project. Nothing needs to be added in it. We build sub-modules in the parent project.

Build an authorization server

Create a springboot project named auth-server.
Introduce dependencies:

        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

Provide a basic configuration of Spring Security

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("admin")
                .and()
                .withUser("pikachues")
                .password(new BCryptPasswordEncoder().encode("123"))
                .roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 开启csrf 允许表单登录
        http.csrf().disable().formLogin();
    }
}

The purpose of this section of configuration is actually to configure users. Here, for convenience and brevity, user information is directly stored in memory. For example, if you want to use WeChat to log in to a third-party website, in this process, you have to log in to WeChat first. Logging in to WeChat requires your username/password information, then what we configure here is actually the user's username/password/role information.

After the basic user information is configured, the authorization server is configured next:

@Configuration
public class AccessTokenConfig {
    /**
     * 你生成的token要往哪里存,可以存放在redis中,也可以存放在内存中
     * 这里存放于内存中
     * @return
     */
    @Bean
    TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
}


@Configuration
@EnableAuthorizationServer  //开启授权服务器的自动化配置
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter  {

    @Autowired
    TokenStore tokenStore;

    @Autowired
    ClientDetailsService clientDetailsService;

    /**
     * 对授权服务器进一步配置
     * 主要用来配置 Token 的一些基本信息,例如 Token 是否支持刷新、Token 的存储位置、Token 的有效期以及刷新 Token 的有效期等等
     * @return
     */
    @Bean
    AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);
        services.setSupportRefreshToken(true);
        services.setTokenStore(tokenStore);
        services.setAccessTokenValiditySeconds(60*60*2);
        services.setRefreshTokenValiditySeconds(60*60*24*3);
        return services;
    }

    /**
     * 用来配置令牌端点的完全约束,也就是这个端点谁能访问,谁不能访问
     * checkTokenAccess 是指一个 Token 校验的端点,这里设置可以直接访问
     * (在后面,当资源服务器收到 Token 之后,需要去校验 Token 的合法性,就会访问这个端点)。
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll")
                .allowFormAuthenticationForClients();
    }

    /**
     * ClientDetailsServiceConfigurer用来配置客户端详细信息
     * 这里配置客户端校验(客户端信息可以存在数据库中)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("pikachues")
                .secret(new BCryptPasswordEncoder().encode("123"))
                .resourceIds("res1")  // 客户端id
                .authorizedGrantTypes("authorization_code","refresh_token")  //授权类型,四种类型
                .scopes("all")  // 授权范围
                .redirectUris("http://localhost:8083/index.html");  //用户登录成功/失败后调转的地址
    }

    /**
     * AuthorizationServerEndpointsConfigurer 这里用来配置令牌的访问端点和令牌服务。
     * authorizationCodeServices 用来配置授权码的存储
     * tokenServices 用来配置令牌的存储 即 access_token 的存储位置
     * 授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资源的,
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authorizationCodeServices(authorizationCodeServices())
                .tokenServices(tokenServices());
    }

    @Bean
    AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }
}

Resource server construction

Next we build a resource server. In the examples you see on the Internet, most of the resource servers are placed together with the authorization server. If the project is relatively small, this is no problem, but if it is a large project, this approach is not appropriate.

The resource server is used to store the user's resources, such as your image, openid and other information on WeChat. After the user gets the access_token from the authorization server, then the resource server can request data through the access_token.

Create a new springboot project named user-server as the resource server.
rely:

 <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.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

Create a ResourceServerConfig class as the resource server configuration:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 授权服务器跟资源服务器是分开的所以要配这个
     * setCheckTokenEndpointUrl 为token的校验地址
     * @return
     */
    @Bean
    RemoteTokenServices tokenServices(){
        RemoteTokenServices services = new RemoteTokenServices();
        services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        services.setClientId("pikachues");
        services.setClientSecret("123");
        return services;
    }


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("res1").tokenServices(tokenServices());
    }

    /**
     * 配置资源拦截规则
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .anyRequest().authenticated();
    }

}

Next, two interfaces for testing are provided:

@RestController
public class HelloController {


    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
    @GetMapping("/admin/hello")
    public String admin() {
        return "admin";
    }
}

Third-party application building

Create a springboot project named alien-app and introduce the following dependencies:

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

In the resources/templates directory, create index.html with the following content:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>pikachues</title>
</head>
<body>
你好,pikachues!

<!--
 client_id 客户端id
 response_type 表示相应类型,这里是code表示相应一个授权码
 scope 表示授权范围
 redirect_uri 表示授权成功后跳转的地址
 msg  是从资源服务器获取的数据
 -->
<a href="http://localhost:8080/oauth/authorize?client_id=pikachues&response_type=code&scope=all&redirect_uri=http://localhost:8083/index.html">第三方登录</a>
<h1 th:text="${msg}"></h1>

</body>
</html>

Next, define a HelloController for testing:

@Controller
public class HelloController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/index.html")
    public String hello(String code, Model model) {
        if (code != null) {
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("code", code);
            map.add("client_id", "pikachues");
            map.add("client_secret", "123");
            map.add("redirect_uri", "http://localhost:8083/index.html");
            map.add("grant_type", "authorization_code");
            // 根据code去请求access_token
            Map<String,String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
            String access_token = resp.get("access_token");
            System.out.println(access_token);
            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Bearer " + access_token);
            HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
            //携带token从资源服务器获取数据
            ResponseEntity<String> entity = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
            model.addAttribute("msg", entity.getBody());
        }
        return "index";
    }
}

test

First visit http://localhost:8082/index.html, the page is as follows: the
Insert picture description here
above page is equivalent to the following page.
Insert picture description here
Click the third-party login to jump to the following page: the
Insert picture description here
above page is equivalent to the following page:
Insert picture description here
enter the user name: admin, password: 123 in the third-party login page, and click login to jump to the following page.
Insert picture description here
In this page, we can see one Prompt, ask whether to authorize the user pikachues to access the protected resources, we select approve, and then click the Authorize button below. After clicking, the page will automatically jump back to my third-party application:
Insert picture description here
Attention, this At that time, there is an additional code parameter in the address bar, which is the authorization code given by the authorization server. With this authorization code, we can request access_token, and the authorization code will become invalid once used.

At the same time, everyone noticed that there is an admin on the page. This admin is the data requested from the resource server.

Of course, we have configured two users in the authorization server. You can also try to log in with the user pikachues/123, because this user does not have the admin role, so using this user will not be able to get the admin string. The error message is as follows :
Insert picture description here

Oauth2 general interface

Take the authorization mode we have exemplified above as an example. After starting the auth-server, you can see the exposed interface in Idea: The
Insert picture description here
meaning of the interface is as follows:

  • /oauth/authorize: Authorization endpoint
  • /oauth/token: Get the token endpoint, refresh token (refresh_token) is also this interface
  • /oauth/confirm_access: The user confirms the authorization submission endpoint (ask whether to authorize the submission to this endpoint)
  • /oauth/error: Authorization error endpoint
  • /oauth/check_token: Check access_tokenendpoint
  • /oauth/token_key Endpoint that provides the public key

project address

https://github.com/xiaoxiaoshou/Oauth2Demo

This article is mainly to record learning and let yourself better understand Oauth2

This article reference:
https://mp.weixin.qq.com/s/GXMQI59U6uzmS-C0WQ5iUw
https://www.funtl.com/zh/spring-security-oauth2/

Guess you like

Origin blog.csdn.net/qq_41262903/article/details/106300407