In “Spring Security oAuth2(a)” is mainly oAuth some basic concepts and four kinds of authentication, now officially started to complete several main examples, quickly start the Spring Security oAuth2 provided by Spring. Many online tutorials use JWT(JSON Web Tokens) to manage Tokens. Therefore, many people believe that oAuth2 should use JWT to manage Tokens. In fact, this is a very wrong idea. Take a look at the best use scenario for JWT.

Create the oAuth2 sample project

Create Maven project named spring-security-oauth2, pom. XML (spring-security-oauth2) as follows:

<? The XML version = "1.0" encoding = "utf-8"? > < project XMLNS = "http://maven.apache.org/POM/4.0.0" XMLNS: xsi = "http://www.w3.org/2001/XMLSchema-instance" Xsi: schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion > 4.0.0 < / modelVersion > < the parent > < groupId > org. Springframework. Boot < / groupId > The < artifactId > spring - the boot - starter - parent < / artifactId > < version > 2.3.10. RELEASE < / version > < relativePath / > <! -- lookup parent from repository --> </parent> <groupId>cn.tim</groupId> <artifactId>spring-security-oauth2</artifactId> < packaging > pom < / packaging > < version > 1.0 - the SNAPSHOT < / version > < modules > < module > spring ws-security - oauth2 - server < / module > <module>spring-security-oauth2-dependencies</module> </modules> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>cn.tim</groupId> < artifactId > spring ws-security - oauth2 - dependencies < / artifactId > < version > 1.0.0 - the SNAPSHOT < / version > < type > pom < type > <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>Copy the code

Spring-security-oauth2-server dependencies: Spring-security-OAuth2-server < span style = “box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; font-size: 14px! Important; word-break: inherit! Important;”

<? The XML version = "1.0" encoding = "utf-8"? > < project XMLNS = "http://maven.apache.org/POM/4.0.0" XMLNS: xsi = "http://www.w3.org/2001/XMLSchema-instance" Xsi: schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > The < modelVersion > 4.0.0 < / modelVersion > < groupId > cn. Tim < / groupId > < artifactId > spring ws-security - oauth2 - dependencies < / artifactId > < version > 1.0.0 - the SNAPSHOT < / version > <packaging>pom</packaging> <url>https://zouchanglin.cn</url> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <pluginRepositories> <pluginRepository> <id>spring-milestone</id> <name>Spring Milestone</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshot</id> <name>Spring Snapshot</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project>Copy the code

Next, create another child Module, spring-security-OAuth2-server, which acts as an authentication/authorization server Module. The corresponding POM.xml is as follows:

<? The XML version = "1.0" encoding = "utf-8"? > < project XMLNS = "http://maven.apache.org/POM/4.0.0" XMLNS: xsi = "http://www.w3.org/2001/XMLSchema-instance" Xsi: schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > < the parent > < artifactId > spring ws-security - oauth2 < / artifactId > < groupId > cn. Tim < / groupId > < version > 1.0 - the SNAPSHOT < / version > < / parent > < modelVersion > 4.0.0 < / modelVersion > < artifactId > spring ws-security - oauth2 - server < / artifactId > < properties > <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>cn.tim.security.server.OAuth2ServerApplication</mainClass> </configuration> </plugin> </plugins> </build> </project>Copy the code

That’s it. All that’s left is to write the code.

Store tokens based on memory

The memory-based token-based mode is used to demonstrate the most basic operations, so that you can quickly understand the basic concepts of “authentication”, “authorization”, “access token” in oAuth2 authentication server, as shown in the following figure:

Step1: Request the authentication service to obtain the authorization code, and request the address:

GET http://localhost:8080/oauth/authorize? client_id=client&response_type=codeCopy the code

It carries client_id and response_type, the authentication type.

Step2: if the authentication passes, call back the registered URL with the parameter code

GET http://emaple.com/xxx?code=xxxxx

Copy the code

Step3: request the authentication server to obtain the token

POST http://yourClientId:yourSecret@localhost:8080/oauth/token

Copy the code

The parameters are grant_type:authorization_code, code: XXXXX (code is code in step2).

Step4: the authentication server returns the token

{
    "access_token": "5cb673e1-d5c1-4887-b766-0975b29ab74b",
    "token_type": "bearer",
    "expires_in": 43199,
    "scope": "app"
}

Copy the code

Step is the above step, now to write the authentication server configuration code. Create a class inherits AuthorizationServerConfigurerAdapter and add related annotation

package cn.tim.security.server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; @Configuration @EnableAuthorizationServer public class AuthenticationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired private BCryptPasswordEncoder passwordEncoder; / / 1, based on the memory storage token @ Override public void the configure (ClientDetailsServiceConfigurer clients) throws the Exception {/ / configure the client Clients // use memory set.inmemory () // client_id. WithClient ("client") // client_secret The default encoder is used here. Secret (passwordencoder.encode ("secret")) // authorizedGrantTypes("authorization_code") // Authorization scope .scopes("app") // Register the callback address. RedirectUris ("https://example.com"); }}Copy the code

Then the server security configuration, create a class inherits WebSecurityConfigurerAdapter and add comments:

package cn.tim.security.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN")
                .and()
                .withUser("user").password(passwordEncoder().encode("123456")).roles("USER");
    }
}

Copy the code

The overall project structure is as follows:

Now access to obtain the authorization code, open a browser, enter the address:

http://localhost:8080/oauth/authorize?client_id=client&response_type=code

Copy the code

The login page is redirected:

After successful authentication, the user is asked whether to authorize the client

The browser address also contains an authorization code (CODE =j4jmTM). The browser address bar will display the following address:

https://example.com/?code=j4jmTM

Copy the code

Curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=j4jmTM' "http://client:secret@localhost:8080/oauth/token"

Copy the code

The results are as follows:

{
    "access_token":"b15c7d2d-b8ea-41e7-ac09-c7fd1517114b",
    "token_type":"bearer",
    "expires_in":43199,
    "scope":"app"
}

Copy the code

Now try using a PostMan request:

PostMan access_token request PostMan access_token request PostMan access_token request PostMan access_token

Access the authentication server through a GET request to obtain the authorization code -> endpoint: /oauth/authorize

Access the authentication server with the authorization code via a POST request to obtain the token -> Endpoint: /oauth/token

The default endpoint URL is as follows:

/oauth/authorize: Authorization endpoint /oauth/token: token endpoint /oauth/ confirM_access: user confirms authorization submission endpoint /oauth/error: Authorization service error message endpoint /oauth/check_token: Token-resolution endpoint /oauth/token_key for resource service access: endpoint that provides the public key, if you use the JWT tokenCopy the code

Store tokens based on JDBC

Storing tokens based on JDBC is similar to memory, except this time the client information is not written in code, but in the database. Configure the authentication server – configure DataSource: DataSource- configure the token storage mode: TokenStore- > JdbcTokenStore- configure the client read mode: ClientDetailsService – > JdbcClientDetailsService – configure the service endpoint information: AuthorizationServerEndpointsConfigurer – tokenStore: Set the token storage mode – configuration client information: ClientDetailsServiceConfigurer – withClientDetails: set the client configuration read mode 4, configuration, Web security – configuration password encryption mode: BCryptPasswordEncoder – configure authentication information: AuthenticationManagerBuilder5, through the GET request access authentication server for authorization code – endpoint: /oauth/authorize6. Access the authentication server using the authorization code through the POST request to obtain the token – endpoint: /oauth/token

Initialize oAuth2 tables with the following address:

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sq lCopy the code

MySQL > create a table with VARCHAR(256) as the primary key, which exceeds the maximum primary key length (128). Replace LONGVARBINARY with BLOB.

create database oauth2 charset utf8;

use oauth2;

-- used in tests that use HSQL
create table oauth_client_details
(
    client_id               VARCHAR(256) PRIMARY KEY,
    resource_ids            VARCHAR(256),
    client_secret           VARCHAR(256),
    scope                   VARCHAR(256),
    authorized_grant_types  VARCHAR(256),
    web_server_redirect_uri VARCHAR(256),
    authorities             VARCHAR(256),
    access_token_validity   INTEGER,
    refresh_token_validity  INTEGER,
    additional_information  VARCHAR(4096),
    autoapprove             VARCHAR(256)
);

create table oauth_client_token
(
    token_id          VARCHAR(256),
    token             BLOB,
    authentication_id VARCHAR(256) PRIMARY KEY,
    user_name         VARCHAR(256),
    client_id         VARCHAR(256)
);

create table oauth_access_token
(
    token_id          VARCHAR(256),
    token             BLOB,
    authentication_id VARCHAR(256) PRIMARY KEY,
    user_name         VARCHAR(256),
    client_id         VARCHAR(256),
    authentication    BLOB,
    refresh_token     VARCHAR(256)
);

create table oauth_refresh_token
(
    token_id       VARCHAR(256),
    token          BLOB,
    authentication BLOB
);

create table oauth_code
(
    code           VARCHAR(256),
    authentication BLOB
);

create table oauth_approvals
(
    userId         VARCHAR(256),
    clientId       VARCHAR(256),
    scope          VARCHAR(256),
    status         VARCHAR(10),
    expiresAt      TIMESTAMP,
    lastModifiedAt TIMESTAMP
);

-- customized oauth_client_details table
create table ClientDetails
(
    appId                  VARCHAR(256) PRIMARY KEY,
    resourceIds            VARCHAR(256),
    appSecret              VARCHAR(256),
    scope                  VARCHAR(256),
    grantTypes             VARCHAR(256),
    redirectUrl            VARCHAR(256),
    authorities            VARCHAR(256),
    access_token_validity  INTEGER,
    refresh_token_validity INTEGER,
    additionalInformation  VARCHAR(4096),
    autoApproveScopes      VARCHAR(256)
);

Copy the code

Add a client configuration record to table oAUTH_client_details. Set the following fields: client_id: indicates the client id. Client_secret: indicates the client security code. Client authorization scope authorized_grant_types: Client authorization type web_server_redirect_URI: Client_secret The BCryptPasswordEncoder is used to encrypt the client security code. The code is as follows:

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

Copy the code

Druid is the fastest connection pool in the world. HikariCP is the fastest connection pool in the world. Druid is the fastest connection pool in the world.

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.4.5</version> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <! -- Exclude tomcat-JDBC to use HikariCP --> <groupId>org.apache.tomcat</groupId> <artifactId> tomcat-JDBC </artifactId>  </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> < artifactId > mysql connector - Java < / artifactId > < version > 8.0.23 < / version > < / dependency >Copy the code

Then configure the authentication server, create a class inherits AuthorizationServerConfigurerAdapter and add comments:

package cn.tim.security.server.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import javax.sql.DataSource; @Configuration @EnableAuthorizationServer public class AuthenticationServerConfiguration extends AuthorizationServerConfigurerAdapter {/ / 2, based on JDBC access token @ Bean @ Primary @ ConfigurationProperties (prefix = "Spring. Datasource ") public datasource datasource (){// Configure the datasource (note that I am using the HikariCP connection pool). Return datasourceBuilder.create ().build(); } @bean public TokenStore TokenStore(){return JdbcTokenStore(dataSource()); } @bean public ClientDetailsService jdbcClientDetails(){ Return new JdbcClientDetailsService(dataSource()); } @ Override public void the configure (ClientDetailsServiceConfigurer clients) throws the Exception {/ / set the token clients.withClientDetails(jdbcClientDetails()); } @ Override public void the configure (AuthorizationServerEndpointsConfigurer endpoints) {/ / read the client configuration endpoints.tokenStore(tokenStore()); }}Copy the code

Then the server security configuration, create a class inherits WebSecurityConfigurerAdapter and add the relevant comments, and based on the memory store is the same as the token, there is no longer post code. Finally, configure the application.yml connection parameters:

spring: application: name: oauth2-server datasource: type: com.zaxxer.hikari.HikariDataSource jdbcUrl: JDBC: mysql: / / 127.0.0.1:3306 / oauth2? useUnicode=true&characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 12345678 hikari: minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 10 auto-commit: true pool-name: MyHikariCP max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 server: port: 8080Copy the code

The test procedure is the same as above

After the operation succeeds, a record will be added to the database oAUTH_access_token table. The effect picture is as follows:

About the JWT

Before we talk about this question, we need to understand what JWT is. For this part, please refer to Ruan Yifeng’s blog “JSON Web Token Tutorial”. Many people make the mistake of comparing cookies with JWT. The comparison is meaningless — cookies are a storage mechanism and JWT is a token mechanism for encrypting signatures. They are not opposed to each other, but can be used independently or in combination. What you should really compare is session versus JWT and cookies versus Local Storage.

Let’s go back to authentication/authorization:

  • Authentication: Verifies the identity of the target object. For example, logging in to a system with a user name and password is authentication.
  • Authorization: Grants operation rights to target objects that pass the authentication.

To put it more simply: Certification solves the question of who you are, and licensing solves the question of what you can do. HTTP is stateless, so the client and server need to figure out how to make the conversation stateful. For example, only logged-in users have the permission to invoke certain interfaces. After logging in, you need to remember that the user is logged in. The common method is to use the session mechanism.

JWT just packages the user information and expiration information into tokens and does not need to be stored on the server side. How to store JWT on the server side (such as memory and REDis) to realize renewal, logout and other scenarios is equivalent to recreating the session implemented by the Web server again. Besides the encryption and decryption resources of JWT, there is no session mechanism for security.

A clear mobile example comes to mind. If you want to use UDP to implement reliable transport, which means you need to manually implement TCP features, you might as well use TCP directly. Similarly, if you use JWT to manage sessions, you need to manually extend to handle renewals. Log out and so on. It’s better to just use sessions.

So what is the most appropriate application scenario for JWT? That is a one-time verification, such as users need to send an email to activate account, usually need to have a link in the email, this link needs to have the following features: able to identify the user, the link has timeliness (usually only allow a few hours to activate), cannot be tampered with to activate other possible accounts. This scenario is very similar to JWT, where the payload is a fixed parameter for which iss writer and EXP expiration time are prepared. JWT is suitable for simple Restful API authentication. Issue a JWT with a fixed validity period to reduce the risk of JWT exposure. Do not use JWT for server state management, so as to reflect the advantages of JWT stateless.

The resources

Stop Using JWT for Sessions