Статьи

Централизованная авторизация с OAuth2 и непрозрачными токенами с использованием Spring Boot 2

Если вы ищете реализацию JWT, пожалуйста, перейдите по этой ссылке

В этом руководстве описан процесс создания централизованного сервера аутентификации и авторизации с Spring Boot 2, также будет предоставлен демонстрационный сервер ресурсов.

Если вы не знакомы с OAuth2, я рекомендую прочитать это.

Pre-REQ

  • JDK 1.8
  • Текстовый редактор или ваш любимый IDE
  • Maven 3.0+

Обзор реализации

Для этого проекта мы будем использовать Spring Security 5 через Spring Boot. Если вы знакомы с более ранними версиями, может пригодиться это руководство по миграции весенней загрузки .

OAuth2 Терминология

  • Владелец ресурса
    • Пользователь, который авторизует приложение для доступа к своей учетной записи. Доступ ограничен scope .
  • Ресурсный сервер :
    • Сервер, который обрабатывает аутентифицированные запросы после того, как client получил access token .
  • клиент
    • Приложение, которое обращается к защищенным ресурсам от имени владельца ресурса.
  • Сервер авторизации
    • Сервер, который выдает токены доступа после успешной аутентификации client и resource owner и авторизации запроса.
  • Токен доступа
    • Уникальный токен, используемый для доступа к защищенным ресурсам
  • Сфера
    • Разрешение
  • Тип гранта

Сервер авторизации

Для создания нашего Authorization Server мы будем использовать Spring Security 5.x через Spring Boot 2.0.x.

зависимости

Вы можете перейти к start.spring.io и создать новый проект, а затем добавить следующие зависимости:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
     
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
 
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>  
    </dependencies>

База данных

Для этого руководства мы будем использовать базу данных H2 .
Здесь вы можете найти справочную схему SQL OAuth2, необходимую для Spring Security.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CREATE TABLE IF NOT EXISTS oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256) NOT NULL,
  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(4000),
  autoapprove VARCHAR(256)
);
 
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256),
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);
 
CREATE TABLE IF NOT EXISTS oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);
 
CREATE TABLE IF NOT EXISTS oauth_code (
  code VARCHAR(256), authentication BLOB
);

А затем добавьте следующую запись

1
2
3
-- The encrypted client_secret it `secret`
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
  VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);

client_secret выше client_secret был создан с использованием bcrypt .
Префикс {bcrypt} является обязательным, потому что мы будем использовать новую функцию DelegatingPasswordEncoder в Spring Security 5.x.

Ниже вы можете найти справочную SQL-схему User and Authority используемую Spring org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl .

01
02
03
04
05
06
07
08
09
10
11
12
13
CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(256) NOT NULL,
  password VARCHAR(256) NOT NULL,
  enabled TINYINT(1),
  UNIQUE KEY unique_username(username)
);
 
CREATE TABLE IF NOT EXISTS authorities (
  username VARCHAR(256) NOT NULL,
  authority VARCHAR(256) NOT NULL,
  PRIMARY KEY(username, authority)
);

Как и прежде, добавьте следующие записи для пользователя и его полномочий.

1
2
3
-- The encrypted password is `pass`
INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');

Конфигурация Spring Security

Добавьте следующий класс конфигурации Spring.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
 
import javax.sql.DataSource;
 
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
 
    private final DataSource dataSource;
 
    private PasswordEncoder passwordEncoder;
    private UserDetailsService userDetailsService;
 
    public WebSecurityConfiguration(final DataSource dataSource) {
        this.dataSource = dataSource;
    }
 
    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        if (passwordEncoder == null) {
            passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
        return passwordEncoder;
    }
 
    @Bean
    public UserDetailsService userDetailsService() {
        if (userDetailsService == null) {
            userDetailsService = new JdbcDaoImpl();
            ((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
        }
        return userDetailsService;
    }
 
}

Цитата из весеннего блога :

Аннотация @EnableWebSecurity и WebSecurityConfigurerAdapter работают вместе для обеспечения безопасности на основе Интернета.

Если вы используете Spring Boot, объект DataSource будет автоматически настроен, и вы можете просто внедрить его в класс, а не определять его самостоятельно. он должен быть JdbcDaoImpl в UserDetailsService в котором будет использоваться предоставленный JdbcDaoImpl предоставленный Spring Security, при необходимости вы можете заменить его своей собственной реализацией.

Так как AuthenticationManager Spring Security требуется некоторыми автоматически настроенными Spring @Bean , необходимо переопределить метод authenticationManagerBean а аннотировать — как @Bean .

PasswordEncoder будет обрабатываться PasswordEncoderFactories.createDelegatingPasswordEncoder() который обрабатывает несколько кодировщиков и делегатов паролей на основе префикса, в нашем примере мы начинаем с паролей {bcrypt} .

Настройка сервера авторизации

Сервер авторизации проверяет учетные данные client и user и предоставляет токены.

Добавьте следующий класс конфигурации Spring.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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 AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
 
    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;
 
    private TokenStore tokenStore;
 
    public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
                                            final AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }
 
    @Bean
    public TokenStore tokenStore() {
        if (tokenStore == null) {
            tokenStore = new JdbcTokenStore(dataSource);
        }
        return tokenStore;
    }
 
    @Bean
    public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setAuthenticationManager(authenticationManager);
        return tokenServices;
    }
 
    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }
 
    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore());
    }
 
    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer.passwordEncoder(passwordEncoder)
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
 
}

Информация о пользователе Конечная точка

Теперь нам нужно определить конечную точку, где маркер авторизации может быть декодирован в объект Authorization , для этого добавьте следующий класс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.security.Principal;
 
@RestController
@RequestMapping("/profile")
public class UserController {
 
    @GetMapping("/me")
    public ResponseEntity<principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }
 
}
</principal>

Конфигурация сервера ресурсов

На сервере ресурсов размещаются HTTP-ресурсы, в которых может быть документ, фотография или что-то еще, в нашем случае это будет REST API, защищенный OAuth2.

зависимости

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
            
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>               
   </dependencies>

Определение нашего защищенного API

Приведенный ниже код определяет конечную точку /me и возвращает объект Principal и для этого требуется, чтобы аутентифицированный пользователь имел ROLE_USER к ROLE_USER .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.security.Principal;
 
@RestController
@RequestMapping("/me")
public class UserController {
 
    @GetMapping
    @PreAuthorize("hasRole('ROLE_USER')")
    public ResponseEntity<Principal> get(final Principal principal) {
        return ResponseEntity.ok(principal);
    }
 
}

Аннотация @PreAuthorize проверяет, имеет ли пользователь заданную роль перед выполнением кода, чтобы заставить его работать, необходимо включить аннотации prePost , для этого добавьте следующий класс:

1
2
3
4
5
6
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {
 
}

Важной частью здесь является @EnableGlobalMethodSecurity(prePostEnabled = true) , для флага prePostEnabled по умолчанию установлено значение false , если для этого @PreAuthorize значение @PreAuthorize аннотация @PreAuthorize будет работать.

Конфигурация сервера ресурсов

Теперь давайте добавим конфигурацию Spring для сервера ресурсов.

01
02
03
04
05
06
07
08
09
10
11
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
 
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
 
}

Аннотация @EnableResourceServer из javadoc:

Удобная аннотация для серверов ресурсов OAuth2, включающая фильтр Spring Security, который аутентифицирует запросы через входящий токен OAuth2. Пользователи должны добавить эту аннотацию и предоставить @Bean типа {@link ResourceServerConfigurer} (например, через {@link ResourceServerConfigurerAdapter}), который определяет детали ресурса (пути URL и идентификатор ресурса). Чтобы использовать этот фильтр, вы должны {@link EnableWebSecurity} где-то в вашем приложении, либо там, где вы используете эту аннотацию, либо где-то еще.

Теперь, когда у нас есть весь необходимый код, нам нужно настроить RemoteTokenServices. К счастью для нас, Spring предоставляет свойство конфигурации, в котором мы можем задать URL-адрес, по которому токены могут быть преобразованы в объект Authentication .

1
2
3
4
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/profile/me

Тестируем все вместе

Чтобы протестировать все вместе, нам нужно также раскрутить Authorization Server Resource Server , в моей настройке он будет работать на портах 9001 и 9101 соответственно.

Генерация токена

1
2
3
4
5
6
7
8
9
$ curl -u clientId:secret -X POST localhost:9001/oauth/token\?grant_type=password\&username=user\&password=pass
 
{
  "access_token" : "e47876b0-9962-41f1-ace3-e3381250ccea",
  "token_type" : "bearer",
  "refresh_token" : "8e17a71c-cb39-4904-8205-4d9f8c71aeef",
  "expires_in" : 299,
  "scope" : "read write"
}

Доступ к ресурсу

Теперь, когда вы сгенерировали токен, скопируйте access_token и добавьте его в запрос в HTTP-заголовке Authorization , например:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
$ curl -i localhost:9101/me -H "Authorization: Bearer c06a4137-fa07-4d9a-97f9-85d1ba820d3a"
 
{
  "authorities" : [ {
    "authority" : "ROLE_USER"
  } ],
  "details" : {
    "remoteAddress" : "127.0.0.1",
    "sessionId" : null,
    "tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
    "tokenType" : "Bearer",
    "decodedDetails" : null
  },
  "authenticated" : true,
  "userAuthentication" : {
    "authorities" : [ {
      "authority" : "ROLE_USER"
    } ],
    "details" : {
      "authorities" : [ {
        "authority" : "ROLE_USER"
      } ],
      "details" : {
        "remoteAddress" : "127.0.0.1",
        "sessionId" : null,
        "tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
        "tokenType" : "Bearer",
        "decodedDetails" : null
      },
      "authenticated" : true,
      "userAuthentication" : {
        "authorities" : [ {
          "authority" : "ROLE_USER"
        } ],
        "details" : {
          "grant_type" : "password",
          "username" : "user"
        },
        "authenticated" : true,
        "principal" : {
          "password" : null,
          "username" : "user",
          "authorities" : [ {
            "authority" : "ROLE_USER"
          } ],
          "accountNonExpired" : true,
          "accountNonLocked" : true,
          "credentialsNonExpired" : true,
          "enabled" : true
        },
        "credentials" : null,
        "name" : "user"
      },
      "clientOnly" : false,
      "oauth2Request" : {
        "clientId" : "clientId",
        "scope" : [ "read", "write" ],
        "requestParameters" : {
          "grant_type" : "password",
          "username" : "user"
        },
        "resourceIds" : [ ],
        "authorities" : [ {
          "authority" : "ROLE_CLIENT"
        } ],
        "approved" : true,
        "refresh" : false,
        "redirectUri" : null,
        "responseTypes" : [ ],
        "extensions" : { },
        "grantType" : "password",
        "refreshTokenRequest" : null
      },
      "credentials" : "",
      "principal" : {
        "password" : null,
        "username" : "user",
        "authorities" : [ {
          "authority" : "ROLE_USER"
        } ],
        "accountNonExpired" : true,
        "accountNonLocked" : true,
        "credentialsNonExpired" : true,
        "enabled" : true
      },
      "name" : "user"
    },
    "authenticated" : true,
    "principal" : "user",
    "credentials" : "N/A",
    "name" : "user"
  },
  "principal" : "user",
  "credentials" : "",
  "clientOnly" : false,
  "oauth2Request" : {
    "clientId" : null,
    "scope" : [ ],
    "requestParameters" : { },
    "resourceIds" : [ ],
    "authorities" : [ ],
    "approved" : true,
    "refresh" : false,
    "redirectUri" : null,
    "responseTypes" : [ ],
    "extensions" : { },
    "grantType" : null,
    "refreshTokenRequest" : null
  },
  "name" : "user"
}

сноска

Опубликовано на Java Code Geeks с разрешения Маркоса Барберо, партнера нашей программы JCG . См. Оригинальную статью здесь: Централизованная авторизация с OAuth2 + непрозрачными токенами с использованием Spring Boot 2

Мнения, высказанные участниками Java Code Geeks, являются их собственными.