Не гидрология! Никто так не учил Spring Security и OAuth 2.0

Spring Boot
Не гидрология! Никто так не учил Spring Security и OAuth 2.0

В процессе обучения настройке Spring Security, после прочтения официальных документов Spring Security, «Spring Security в действии» и «OAuth 2 в действии» и в сочетании с исходным кодом, чтобы узнать рабочий процесс Spring Security и отсортировать Чтобы преобразовать эти знания в изображения и текст, мне потребовался целый месяц, я действительно не ожидал, что в Spring Security будет так много контента.

🌊0 Обзор

0.1 Анализ процесса

0.1.1 Процесс аутентификации пароля пользователя

Предположим, что доступ к интерфейсу — /private, интерфейс входа — /login, а страница входа — login.html.

new-用户名密码验证.jpg

  1. Во-первых, пользователь делает неавторизованный запрос к своему неавторизованному ресурсу /private.
  2. Когда пользователь отправляет свое имя пользователя и пароль, UsernamePasswordAuthenticationFilter создает UsernamePasswordAuthenticationToken, извлекая имя пользователя и пароль из HttpServletRequest.
  3. Затем передайте UsernamePasswordAuthenticationToken в AuthenticationManager для аутентификации.
  4. Настройте подкласс AuthenticationProvider для ProviderManager DaoAuthenticationProvider
  5. DaoAuthenticationProvider находит UserDetails в JDBCUserDetailManager, подклассе UserDetailsService
  6. JDBCUserDetailManager находит пользователей в базе данных и возвращает информацию о них.
  7. Если пользователь найден, PasswordEncoder сравнит информацию о пользователе, отправленную запросом пользователя, с информацией о пользователе, считанной из базы данных,Если проверка прошла успешно, перейдите к шагу 8.,Ошибка проверки на шаге 10
  8. Информация о пользователе будет помещена в UsernamePasswordAuthenticationToken и возвращена,Перейти к шагу 9
  9. Поместите UsernamePasswordAuthenticationToken в SecurityContextHolder
  10. Указывает, что неаутентифицированный запрос был отклонен путем создания AccessDeniedException ,Перейти к шагу 11
  11. Отправить перенаправление на страницу входа Location:/login с настроенной AuthenticationEntryPoint,Перейти к шагу 12
  12. Браузер запросит страницу входа, на которую он был перенаправлен GET /login,Перейдите к шагу 13
  13. Вернуться на страницу входа login.html

0.1.2 Базовый процесс HTTP-аутентификации

new-basic认证.jpg

  1. Во-первых, пользователь делает неавторизованный запрос к своему неавторизованному ресурсу /private.
  2. Когда пользователь отправляет свое имя пользователя и пароль, BasicAuthenticationFilter создает UsernamePasswordAuthenticationToken, извлекая имя пользователя и пароль из HttpServletRequest.
  3. Затем передайте UsernamePasswordAuthenticationToken в AuthenticationManager для аутентификации.Процесс аутентификации аналогичен UserPasswordAuthentication.Если проверка прошла успешно, перейдите к шагу 4.,Если проверка не удалась, перейдите к шагу 6.
  4. Сохранить аутентификацию в SecurityContextHolder,Перейти к шагу 5
  5. Вызов RememberMeServices.loginSuccess без операции, если функция «запомнить меня» не настроена; BasicAuthenticationFilter вызывает FilterChain.doFilter(request,response), чтобы продолжить работу с остальной логикой приложения.
  6. Очищает SecurityContextHolder; вызывает RememberMeServices.loginFail, который не работает, если функция «запомнить меня» не настроена; FilterSecurityInterceptor указывает, что неаутентифицированные запросы были отклонены путем создания AccessDeniedException ,Перейдите к шагу 7
  7. AuthenticationEntryPoint вызывается для повторной отправки WWW-Authenticate.

0.1.3 Процесс авторизации

new-授权.jpg

  1. FilterSecurityInterceptor получает аутентификацию от SecurityContextHolder
  2. FilterSecurityInterceptor создает FilterInvocation из полученных HttpServletRequest, HttpServletResponse и FilterChain.
  3. FilterSecurityInterceptor передает FilterInvocation в SecurityMetadataSource для получения нескольких ConfigAttributes.
  4. FilterSecurityInterceptor передает аутентификацию, FilterInvocation и ConfigAttributes в AccessDecisionManager, если доступ авторизован, FilterSecurityInterceptor продолжает выполнять FilterChain,В противном случае перейдите к шагу 5
  5. Выдает исключение AccessDeniedException

0.1.4 Процесс OAuth2

Сервер ресурсов (с использованием JWT)

new-oauth2—resource.jpg

  1. Пользователь делает неаутентифицированный запрос к своему неавторизованному ресурсу /private
  2. Когда пользователь отправляет токен носителя, BearerTokenAuthenticationFilter создает BearerTokenAuthenticationToken, извлекая токен из HttpServletRequest.
  3. HttpServletRequest передается в AuthenticationManagerResolver, который выбирает AuthenticationManager, а BearerTokenAuthenticationToken передается в AuthenticationManager для аутентификации.
  4. ProviderManager настроен на использование подкласса AuthenticationProvider JwtAuthenticationProvider.
  5. JwtAuthenticationProvider использует JwtDecoder для декодирования и проверки Jwt.
  6. JwtAuthenticationProvider использует JwtAuthenticationConverter для преобразования Jwt в коллекцию GrantedAuthority,Если проверка прошла успешно, перейдите к шагу 7.,Если проверка не удалась, перейдите к шагу 9
  7. Возвращенный Authentication представляет собой JwtAuthenticationToken и имеет принцип, который представляет собой Jwt, возвращаемый сконфигурированным JwtDecoder,Перейти к шагу 8
  8. Возвращенный JwtAuthenticationToken будет помещен в SecurityContextHolder, а BearerTokenAuthenticationFilter вызывает FilterChain.doFilter(запрос, ответ) для продолжения остальной логики приложения.
  9. FilterSecurityInterceptor указывает, что неаутентифицированный запрос был отклонен путем создания AccessDeniedException ,Перейти к шагу 10
  10. SecurityContextHolder очищается, и вызывается AuthenticationEntryPoint для повторной отправки заголовка WWW-Authenticate.

Client

new-oauth2—client.jpg

перенаправить
  1. владелец ресурса делает запрос в браузере GET /oauth2/authorization/{registrationId}
  2. OAuth2AuthorizationRequestRedirectFilter передает RegistrationId в качестве параметра и вызывает метод findByRegistrationId() интерфейса ClientRegistrationRepository, а findByRegistrationId() возвращает ClientRegistration
  3. OAuth2AuthorizationRequestRedirectFilter генерирует OAuth2AuthorizationRequest на основе ClientRegistration и вызывает метод saveAuthorizationRequest() класса AuthorizationRequestRepository для совместного использования OAuth2AuthorizationRequest между сеансами.
  4. OAuth2AuthorizationRequestRedirectFilter генерирует URL-адрес из ClientRegistration и отправляет его в конечную точку авторизации сервера авторизации, который возвращает страницу входа.
Обработка перед аутентификацией
  1. Владелец ресурса отправляет информацию о пользователе на странице входа и отправляет запрос на вход.
  2. OAuth2LoginAuthenticationFilter анализирует ответ авторизации от сервера авторизации из запроса и генерирует OAuth2AuthorizationResponse.
  3. OAuth2LoginAuthenticationFilter вызывает метод loadAuthorizationRequest() интерфейса AuthorizationRequestRepository для получения запроса OAuth2AuthorizationRequest.
  4. OAuth2LoginAuthenticationFilter передает RegistrationId в качестве параметра и вызывает метод findByRegistrationId() интерфейса ClientRegistrationRepository, а findByRegistrationId() возвращает ClientRegistration
Процесс сертификации
  1. OAuth2LoginAuthenticationFilter генерирует OAuth2LoginAuthenticationToken, содержащий OAuth2AuthorizationRequest, OAuth2AuthorizationResponse и ClientRegistration.
  2. OAuth2LoginAuthenticationProvider получает токен доступа из конечной точки токена сервера авторизации, вызывая метод getTokenResponse() интерфейса OAuth2AccessTokenResponseClient.
  3. OAuth2LoginAuthenticationProvider получает информацию о пользователе из конечной точки авторизации сервера авторизации, вызывая метод loadUser() интерфейса OAuth2UserService.
  4. OAuth2LoginAuthenticationProvider создает OAuth2LoginAuthenticationToken и возвращает результат аутентификации.
Обработка после аутентификации
  1. OAuth2LoginAuthenticationFilter создает OAuth2AnthenticationToken в соответствии с результатом аутентификации и устанавливает его в SecurityContext.
  2. OAuth2LoginAuthenticationFilter генерирует OAuth2AuthorizedClient в соответствии с результатом аутентификации, вызывает метод saveAuthorizedClinet() службы OAuth2AuthorizedClientService и сохраняет OAuth2AuthorizedClient в области, доступной любому классу.

0.2 Spring Security поставляется с порядком выполнения фильтра

Фильтры, описанные в этой статье, находятся в зеленой рамке.

涉及的过滤器.png

0.3 Структура статьи

new-总体框架.jpg

0.6 Основные зависимости

<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>

🌊1 Управление пользователями

1.1 Используйте UserDetails для описания пользователей

1.1.1 Определение сведений о пользователе

public interface UserDetails extends Serializable {
	// 返回用户凭证
	String getUsername();
	String getPassword();
	
	// 返回用户权限列表
	Collection<? extends GrantedAuthority> getAuthorities();
	
	// 管理用户状态
	// 如果不需要实现以下功能,可以让这些方法都返回 true
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

1.1.2 Определение предоставленных полномочий

public interface GrantedAuthority extends Serializable {
	String getAuthority();
}

1.1.3 Создание экземпляра GrantedAuthority

// SimpleGrantedAuthority 是 GrantedAuthority 的一个基础实现,以字符串形式描述权限
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");
// 或者使用 lambda 表达式
GrantedAuthority g1 = () -> "READ";

1.1.4 Реализация сведений о пользователе

В соответствии с принципом единой ответственности у класса есть только одна ответственность, поэтому, когда пользовательский класс используется только для проверки, это «единый принцип».Если пользовательский класс используется не только для проверки, но также представляет класс сущности в базе данных это мультиответственность

единственная ответственность

  • Наследовать данные пользователя

    public class DummyUser implements UserDetails {
    	@Override
    	public String getUsername() {
    		return "bill";
    	}
    	
    	@Override
    	public String getPassword() {
    		return "12345";
    	}
    	
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		return List.of(() -> "READ");
    	}
    	
    	@Override
    	public boolean isAccountNonExpired() {
    		return true;
    	}
    	
    	@Override
    	public boolean isAccountNonLocked() {
    		return true;
    	}
    	
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return true;
    	}
    	
    	@Override
    	public boolean isEnabled() {
    		return true;
    	}
    	
    }
    
  • Используйте статические методы класса User

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

    • пример

      // 至少要提供 username 和 password,且 username 不能为空串
      // 此处 User.withUsername("bill") 返回的是 User.UserBuilder 的实例(见下方“原理”)
      UserDetails u = User.withUsername("bill")
      					.password("12345")
      					.authorities("read", "write")
      					.accountExpired(false)
      					.disabled(true)
      					.build();
      
    • принцип

      User.UserBuilder builder1 = User.withUsername("bill");
      UserDetails u1 = builder1
      					.password("12345")
      					.authorities("read", "write")
      					.passwordEncoder(p -> encode(p))
      					.accountExpired(false)
      					.disabled(true)
      					.build();
      

несколько обязанностей

  • Наследовать сведения о пользователе и использовать шаблон декоратора

    • класс сущности базы данных

      public class MyUser {
      	private Long id;
      	private String username;
      	private String password;
      	private String authority;
      	
      	// 忽略 getters and setters
      }
      
    • Создайте пользовательский класс с двумя обязанностями

      public class SecurityUser implements UserDetails {
      	private final MyUser user;
      	
      	public SecurityUser(MyUser user) {
      		this.user = user;
      	}
      	
      	@Override
      	public String getUsername() {
      		return user.getUsername();
      	}
      	
      	@Override
      	public String getPassword() {
      		return user.getPassword();
      	}
      	
      	@Override
      	public Collection<? extends GrantedAuthority> getAuthorities() {
      		return List.of(() -> user.getAuthority());
      	}
      	
      	// 忽略代码
      }
      

1.2 Управление пользователями с помощью JDBCUserdetailsManager

1.2.1 Определение UserDetailsService

public interface UserDetailsService {
	// UsernameNotFoundException 是运行时异常,继承自 AuthenticationException(所有验证过程异常的父类)
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

1.2.2 Определение UserDetailsManager

public interface UserDetailsManager extends UserDetailsService {
	void createUser(UserDetails user);
	void updateUser(UserDetails user);
	void deleteUser(String username);
	void changePassword(String oldPassword, String newPassword);
	boolean userExists(String username);
}	

1.2.3 JDBCUserdetailsManager в классе конфигурации

  • @Configuration
    public class ProjectConfig {
    	@Bean
    	public UserDetailsService userDetailsService(DataSource dataSource) {
    		return new JdbcUserDetailsManager(dataSource);
    	}
    	
    	@Bean
    	public PasswordEncoder passwordEncoder() {
    		return NoOpPasswordEncoder.getInstance();
    	}
    }
    
  • Если вы хотите изменить оператор запроса к базе данных по умолчанию

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {
    	String usersByUsernameQuery = "select username, password, enabled from users where username = ?";
    	String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
    	var userDetailsManager = new JdbcUserDetailsManager(dataSource);
    	userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
    	userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
    	return userDetailsManager;
    }
    

🌊2 Обработка паролей

2.1 Реализация PasswordEncoder

2.1.1 Определение PasswordEncoder

public interface PasswordEncoder {
	// 返回编码结果
	String encode(CharSequence rawPassword);
	
	// 比对密码
	boolean matches(CharSequence rawPassword, String encodedPassword);
	
	// 默认为 false,如果重写改成返回 true,已经编码过的密码会被再一次编码,以达到更安全的目的
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

2.1.2 Реализация PasswordEncoder с использованием SHA-512

public class Sha512PasswordEncoder implements PasswordEncoder {
	@Override
	public String encode(CharSequence rawPassword) {
		return hashWithSHA512(rawPassword.toString());
	}
	
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		String hashedPassword = encode(rawPassword);
		return encodedPassword.equals(hashedPassword);
	}
	
	private String hashWithSHA512(String input) {
		StringBuilder result = new StringBuilder();
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-512");
			byte [] digested = md.digest(input.getBytes());
			for (int i = 0; i < digested.length; i++) {
				result.append(Integer.toHexString(0xFF & digested[i]));
			}
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("Bad algorithm");
		}
		return result.toString();
	}
}

2.2 Использование подклассов PasswordEncoder

2.2.1 Pbkdf2PasswordEncoder

Использование алгоритма PBKDF2

PasswordEncoder p = new Pbkdf2PasswordEncoder();

// 参数:用于加密的密钥
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");

// 第一个参数:用于加密的密钥
// 第二个参数:给密码编码的迭代次数,默认值为 185000
// 第三个参数:哈希的长度,默认值为 256
// 后面两个参数影响编码结果的强度
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);

2.2.2 BCryptPasswordEncoder

Используйте алгоритм bcrypt

PasswordEncoder p = new BCryptPasswordEncoder();

// 参数会影响哈希操作的迭代次数,若 参数 = n,则 迭代次数 = 2 ^ n (n >= 4 && n <= 31)
PasswordEncoder p = new BCryptPasswordEncoder(4);

2.2.3 SCryptPasswordEncoder

Используйте алгоритм скрипта

PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

image-20211111175613126.png

2.2.4 DelegatingPasswordEncoder

Сохранить -экземпляр в пару ключ-значение

Создать экземпляр

@Configuration
public class ProjectConfig {
	// Omitted code
	@Bean
	public PasswordEncoder passwordEncoder() {
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put("noop", NoOpPasswordEncoder.getInstance());
		encoders.put("bcrypt", new BCryptPasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		
		// 第一个参数:默认的加密算法
		/*
           基于哈希的前缀,DelegatingPassword-Encoder 使用相应的 PasswordEncoder 实现来匹配密码,
           例如 {bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG,
           前缀为 {bcrypt} 所以使用 BCryptPasswordEncoder
        */
		return new DelegatingPasswordEncoder("bcrypt", encoders);
	}
}

Использование PasswordEncoderFactory

// DelegatingPasswordEncoder 的实现,默认使用 bcrypt 算法编码
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

2.2.5 Spring Security Crypto module (SSCM)

Модуль Spring Security Crypto обеспечивает поддержку симметричного шифрования, генерации ключей и кодирования паролей.

KeyGenerators

Класс KeyGenerators предоставляет множество удобных фабричных методов для создания различных типов генераторов ключей.

  • StringKeyGenerator

    Сгенерировать ключ как строку

    • определение

      • public interface StringKeyGenerator {
        	// 创建一个 8 字节的密钥,并将其编码为十六进制字符串
        	String generateKey();
        }
        
    • создавать экземпляр

      • StringKeyGenerator keyGenerator = KeyGenerators.string();
        
        String salt = keyGenerator.generateKey();
        
  • BytesKeyGenerator

    Сгенерировать ключ вида bytes[]

    • определение

      • public interface BytesKeyGenerator {
        	// 以字节数返回密钥长度的方法
        	// 默认生成 8 字节长度的密钥
        	int getKeyLength();
        	
        	byte[] generateKey();
        }
        
    • создавать экземпляр

      Экземпляры BytesKeyGenerator могут быть сгенерированы с использованием KeyGenerators.secureRandom() и KeyGenerators.shared(int length)

      • KeyGenerators.shared(int length)

        Сгенерированный экземпляр BytesKeyGenerator выдает один и тот же результат каждый раз, когда вызывается generateKey(), когда ввод не изменяется.

        • BytesKeyGenerator keyGenerator = KeyGenerators.shared(16);
          
          byte [] key = keyGenerator.generateKey();
          int keyLength = keyGenerator.getKeyLength();
          
      • KeyGenerators.secureRandom()

        Сгенерированный экземпляр BytesKeyGenerator выдает другой результат каждый раз, когда вызывается generateKey(), когда ввод не изменяется.

        • BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
          // 如果想自己设定密钥的长度,可以在 secureRandom 方法中传入参数
          // BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16);
          
          byte [] key = keyGenerator.generateKey();
          int keyLength = keyGenerator.getKeyLength();
          

Encryptor

Класс Encryptors предоставляет фабричные методы для создания симметричных шифраторов.

  • BytesEncryptor

    Шифровать данные как byte[]

    • определение

      • public interface BytesEncryptor {
        	byte[] encrypt(byte[] byteArray);
        	byte[] decrypt(byte[] encryptedByteArray);
        }
        
    • создавать экземпляр

      Экземпляры BytesEncryptor могут быть сгенерированы с помощью Encryptors.standard() и Encryptors.stronger().

      • Encryptors.stronger()

        Использует 256-битный алгоритм AES на основе режима работы Galois/Counter Mode (GCM).

        • String salt = KeyGenerators.string().generateKey();
          String password = "secret";
          String valueToEncrypt = "HELLO";
          
          BytesEncryptor e = Encryptors.stronger(password, salt);
          byte [] encrypted = e.encrypt(valueToEncrypt.getBytes());
          byte [] decrypted = e.decrypt(encrypted);
          
      • Encryptors.standard()

        Использует 256-битный алгоритм AES, основанный на цепочке блоков шифрования (CBC), который считается более слабым методом (по сравнению с strong()).

        • String salt = KeyGenerators.string().generateKey();
          String password = "secret";
          String valueToEncrypt = "HELLO";
          
          BytesEncryptor e = Encryptors.standard(password, salt);
          byte [] encrypted = e.encrypt(valueToEncrypt.getBytes());
          byte [] decrypted = e.decrypt(encrypted);
          
  • TextEncryptor

    зашифрованная текстовая строка

    • определение

      • public interface TextEncryptor {
        	String encrypt(String text);
        	String decrypt(String encryptedText);
        }
        
    • создавать экземпляр

      • Encryptors.text()

        Используйте метод Encryptors.standard() для управления операциями шифрования. Повторные вызовы encrypt() дают разные результаты, если ввод остается прежним.

        • String salt = KeyGenerators.string().generateKey();
          String password = "secret";
          String valueToEncrypt = "HELLO";
          
          TextEncryptor e = Encryptors.text(password, salt);
          String encrypted = e.encrypt(valueToEncrypt);
          String decrypted = e.decrypt(encrypted);
          
      • Encryptors.delux()

        Используйте метод Encryptors.stronger() для управления операциями шифрования. Повторные вызовы encrypt() дают разные результаты, если ввод остается прежним.

        • String salt = KeyGenerators.string().generateKey();
          String password = "secret";
          String valueToEncrypt = "HELLO";
          
          TextEncryptor e = Encryptors.delux(password, salt);
          String encrypted = e.encrypt(valueToEncrypt);
          String decrypted = e.decrypt(encrypted);
          
      • Encryptors.queryableText()

        Повторные вызовы encrypt() дают тот же результат, если ввод не изменился.

        • String salt = KeyGenerators.string().generateKey();
          String password = "secret";
          String valueToEncrypt = "HELLO";
          
          TextEncryptor e = Encryptors.queryableText(password, salt);
          String encrypted1 = e.encrypt(valueToEncrypt);
          String encrypted2 = e.encrypt(valueToEncrypt);
          // 这里 encrypted1 等于 encrypted2
          

🌊3 Аутентификация пользователя

3.1 Реализация AuthenticationProvider

3.1.1 Определение аутентификации

Интерфейс проверки подлинности представляет собой событие запроса проверки подлинности и содержит сведения об объекте, запрашивающем доступ к приложению.

public interface Authentication extends Principal, Serializable {
	// 返回已验证请求的授予权限集合。
	Collection<? extends GrantedAuthority> getAuthorities();
	// 返回认证过程中需要的密钥
	Object getCredentials();
	// 返回关于请求的更多信息
	Object getDetails();
	Object getPrincipal();
	// 认证结束返回 true,认证还在进行中则返回 false
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

3.1.2 Определение AuthenticationProvider

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	
	// 如果当前 AuthenticationProvider 支持参数内的类型,则可以实现此方法以返回 true
	boolean supports(Class<?> authentication);
}

3.1.3 Реализация

шаг

  • Создайте класс, реализующий интерфейс AuthenticationProvider.
  • Повторно поддерживает(Класс>с) и аутентифицирует(Аутентификация а)
  • Зарегистрируйте экземпляр класса, созданный на первом шаге, в файле конфигурации SpringBoot.

код

  • @Component
    public class CustomAuthenticationProvider implements AuthenticationProvider {
    	@Autowired
    	private UserDetailsService userDetailsService;
    	
    	@Autowired
    	private PasswordEncoder passwordEncoder;
    	
    	@Override
    	public Authentication authenticate(Authentication authentication) {
    		String username = authentication.getName();
    		String password = authentication.getCredentials().toString();
    		UserDetails u = userDetailsService.loadUserByUsername(username);
    		if (passwordEncoder.matches(password, u.getPassword())) {
    			return new UsernamePasswordAuthenticationToken(username, password, u.getAuthorities());
    		} else {
    			// 验证失败时要抛出 AuthenticationException 异常
    			throw new BadCredentialsException("Something went wrong!");
    		}
    	}
    	
    	// 如果接收的 Authentication 的对象不被你的 AuthenticationProvider 实现支持,则返回 false
    	@Override
    	public boolean supports(Class<?> authenticationType) {
    		return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    	}
    }
    
  • @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Autowired
    	private AuthenticationProvider authenticationProvider;
    	
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) {
    		auth.authenticationProvider(authenticationProvider);
    	}
    	// 省略代码
    }
    

3.2 Реализация контекста безопасности

3.2.1 SecurityContext

Когда AuthenticationManager завершит аутентификацию, экземпляр Authentication будет сохранен в SecurityContext.

определение

public interface SecurityContext extends Serializable {
	Authentication getAuthentication();
	void setAuthentication(Authentication authentication);
}

Стратегии управления объектами аутентификации

  • Установите стратегию, используя SecurityContextHolder.setStrategyName()

    @Configuration
    public class ProjectConfig {
    	@Bean
    	public InitializingBean initializingBean() {
    		return () -> SecurityContextHolder.setStrategyName(
    		SecurityContextHolder.MODE_THREADLOCAL);
    	}
    }
    
  • три стратегии

    • MODE_THREADLOCAL

      Политика SecurityContext по умолчанию

      • Функции

        • Управляется с помощью ThreadLocal, поэтому каждый поток хранит свой собственный объект аутентификации.
      • создавать экземпляр

        • Получить экземпляр SecurityContext из SecurityContextHolder

          @GetMapping("/hello")
          public String hello() {
          	SecurityContext context = SecurityContextHolder.getContext();
          	Authentication a = context.getAuthentication();
          	return "Hello, " + a.getName() + "!";
          }
          
        • Spring вводит значение аутентификации в параметр метода

          @GetMapping("/hello")
          public String hello(Authentication a) {
          	return "Hello, " + a.getName() + "!";
          }
          
    • MODE_INHERITABLETHREADLOCAL

      Каждый поток хранит свой собственный объект Authentication.Если текущий поток A создает новый поток B, то объект Authentication A будет скопирован в B

      • Поток, выполняющий метод, отличается от потока, обрабатывающего запрос.

        • @GetMapping("/bye")
          @Async
          public void goodbye() {
          	SecurityContext context = SecurityContextHolder.getContext();
          	String username = context.getAuthentication().getName();
          	// do something with the username
          }
          
        • Чтобы включить асинхронность, вам нужно добавить аннотацию @EnableAsync в класс конфигурации

          @Configuration
          @EnableAsync
          public class ProjectConfig {
          	@Bean
          	public InitializingBean initializingBean() {
          		// 使用 SecurityContextHolder.setStrategyName() 设置策略
          		return () -> SecurityContextHolder.setStrategyName(
          		SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
          	}
          }
          
      • Уведомление

        • Потоки, созданные самой инфраструктурой Spring, обеспечивают успех этой стратегии (как показано выше).
        • Если вы создаете поток в своем собственном коде, возникнет ситуация, когда SecurityContextHolder.getContext() вернет значение null, потому что фреймворк не знает созданный вами поток.Если вы хотите, чтобы ваш собственный поток сделал эту стратегию успешной, пожалуйста, посмотрите в классе декоратора, представленном далее
    • MODE_GLOBAL

      Все потоки совместно используют объект аутентификации

      • Функции
        • Имеет проблемы с безопасностью потоков
  • Обработка асинхронности с классами декораторов

    • DelegatingSecurityContextRunnable

      • Функции

        • Унаследовано от Runnable
        • Украсьте объект Runnable
        • Оберните Runnable логикой, используемой для установки SecurityContext перед вызовом Runnable, а затем удалите SecurityContext после завершения вызова.
      • применение

        @GetMapping("/ciao")
        public String ciao() throws Exception {
        	Runnable task = () -> {
        		SecurityContext context = SecurityContextHolder.getContext();
        		return context.getAuthentication().getName();
        	};
        	
        	ExecutorService e = Executors.newCachedThreadPool();
        	try {
        		var contextTask = new DelegatingSecurityContextRunnable(task);
        		return "Ciao, " + e.submit(contextTask).get() + "!";
        	} finally {
        		e.shutdown();
        	}
        }
        
    • DelegatingSecurityContextCallable

      • Функции

        • Унаследовано от Callable
        • украсить вызываемый объект
        • Оберните Callable логикой, используемой для установки SecurityContext перед вызовом Callable, а затем удалите SecurityContext после завершения вызова.
      • применение

        @GetMapping("/ciao")
        public String ciao() throws Exception {
        	Callable<String> task = () -> {
        		SecurityContext context = SecurityContextHolder.getContext();
        		return context.getAuthentication().getName();
        	};
        	
        	ExecutorService e = Executors.newCachedThreadPool();
        	try {
        		var contextTask = new DelegatingSecurityContextCallable<>(task);
        		return "Ciao, " + e.submit(contextTask).get() + "!";
        	} finally {
        		e.shutdown();
        	}
        }
        
    • DelegatingSecurityContextExecutorService

      • Функции

        • Реализованный ExecutorService
        • Украсьте объект ExecutorService
        • Он оборачивает каждый Runnable в DelegatingSecurityContextRunnable и каждый Callable в DelegatingSecurityContextCallable.
      • применение

        @GetMapping("/hola")
        public String hola() throws Exception {
        	Callable<String> task = () -> {
        		SecurityContext context = SecurityContextHolder.getContext();
        		return context.getAuthentication().getName();
        	};
        	ExecutorService e = Executors.newCachedThreadPool();
        	e = new DelegatingSecurityContextExecutorService(e);
        	try {
        		return "Hola, " + e.submit(task).get() + "!";
        	} finally {
        		e.shutdown();
        	}
        }
        
    • DelegatingSecurityContextScheduledExecutorService

      • Функции
        • для запланированных задач
        • Реализует ScheduledExecutorService
        • Украсьте объект ScheduledExecutorService
        • Он оборачивает каждый Runnable в DelegatingSecurityContextRunnable и каждый Callable в DelegatingSecurityContextCallable.

3.3 HTTP Basic authentication

image-20211116095452110.png

3.3.1 Базовая настройка HTTP

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic(c -> {
			// 配置域名
			c.realmName("OTHER");
		});
		
		http.authorizeRequests().anyRequest().authenticated();
	}
}

3.3.2 AuthenticationEntryPoint

Функции

  • AuthenticationEntryPoint используется для определения действия при сбое аутентификации.
  • В архитектуре Spring Security он используется непосредственно компонентом ExceptionTranslationManager, который обрабатывает любое исключение AccessDenied-Exception и AuthenticationException, выброшенное в цепочке фильтров.

создавать экземпляр

  • public class CustomEntryPoint implements AuthenticationEntryPoint {
    	@Override
    	public void commence(
    		HttpServletRequest httpServletRequest,
    		HttpServletResponse httpServletResponse,
    		AuthenticationException e)
    		throws IOException, ServletException {
    			httpServletResponse.addHeader("message", "Luke, I am your father!");
    			httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
    		}
    }
    
  • В методе HTTP Basic класса конфигурации

    • @Override
      protected void configure(HttpSecurity http) throws Exception {
      	http.httpBasic(c -> {
      		c.realmName("OTHER");
      		c.authenticationEntryPoint(new CustomEntryPoint());
      	});
      	http.authorizeRequests()
      		.anyRequest()
      		.authenticated();
      }
      

3.4 form-based login authentication

Предоставьте пользователю форму входа для ввода аутентификационных данных.

image-20211116095320300.png

3.4.1 Настройка входа на основе формы

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.formLogin();
		http.authorizeRequests().anyRequest().authenticated();
	}
}

3.4.2 Реализация AuthenticationSuccessHandler

Используется для настройки логики после успешной проверки

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(
		HttpServletRequest httpServletRequest,
		HttpServletResponse httpServletResponse,
		Authentication authentication)
			throws IOException {
				// 以下代码的逻辑是验证成功后如果用户有读权限就重定向到其他页面
				var authorities = authentication.getAuthorities();
				var auth = authorities.stream()
									  .filter(a -> a.getAuthority().equals("read"))
									  .findFirst();
				if (auth.isPresent()) {
					httpServletResponse.sendRedirect("/home");
				} else {
					httpServletResponse.sendRedirect("/error");
				}
			}
}

3.4.3 Реализация обработчика AuthenticationFailureHandler

Используется для настройки логики после сбоя проверки

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
	@Override
	public void onAuthenticationFailure(
		HttpServletRequest httpServletRequest,
		HttpServletResponse httpServletResponse,
		AuthenticationException e) {
			// 在响应的请求头添加信息
			httpServletResponse.setHeader("failed", LocalDateTime.now().toString());
	}
}

3.4.4 Зарегистрируйте объект-обработчик в классе конфигурации

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
	@Autowired
	private CustomAuthenticationFailureHandler authenticationFailureHandler;
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.formLogin()
			.successHandler(authenticationSuccessHandler)
			.failureHandler(authenticationFailureHandler);
			
		http.authorizeRequests()
			.anyRequest().authenticated();
	}
}

3.4.5 Если вы хотите использовать HTTP-запрос для входа в систему, вам необходимо одновременно поддерживать HTTP-аутентификацию.

@Override
protected void configure(HttpSecurity http) throws Exception {
	// 同时支持 HTTP Basic 验证 和 form-based login 验证
	http.formLogin()
		.successHandler(authenticationSuccessHandler)
		.failureHandler(authenticationFailureHandler)
	.and()
		.httpBasic();
		
	http.authorizeRequests().anyRequest().authenticated();
}

🌊4 Настроить авторизацию

4.1 Установка разрешений или ролей

4.1.1 Установка разрешений

var user1 = User.withUsername("john")
				.password("12345")
				.authorities("READ","WRITE")
				.build();

4.1.2 Настройка ролей

Используйте метод авторитетов (строка... авторитеты)

Примечание. Настройка ролей с помощью этого метода требует добавления префикса к строке роли.ROLE_

var user1 = User.withUsername("john")
				.password("12345")
				.authorities("ROLE_ADMIN")
				.build();

Используйте метод roles(String... roles)

var user1 = User.withUsername("john")
				.password("12345")
				.roles("ADMIN")
				.build();

4.2 Ограничения разрешений

4.2.1 На основе разрешений пользователя

Есть три способа

  • hasAuthority(String authority)

    • Функции

      • Только пользователи с полномочием могут звонить на терминал
    • применение

      @Configuration
      public class ProjectConfig extends WebSecurityConfigurerAdapter {
      	// Omitted code
      	@Override
      	protected void configure(HttpSecurity http) throws Exception {
      		http.httpBasic();
      		http.authorizeRequests()
      			.anyRequest()			  // 端点限制
      			.hasAuthority("WRITE");   // 权限限制
      	}
      }
      
  • hasAnyAuthority(String... authorities)

    • Функции

      • Пользователь имеет хотя бы одно из полномочий для вызова терминала.
    • применение

      @Configuration
      public class ProjectConfig extends WebSecurityConfigurerAdapter {
      	// Omitted code
      	@Override
      	protected void configure(HttpSecurity http) throws Exception {
      		http.httpBasic();
      		http.authorizeRequests()
      			.anyRequest()						// 端点限制
      			.hasAnyAuthority("WRITE", "READ");  // 权限限制
      	}
      }
      
  • access(String attribute)

    • Функции

      • Гибкая поддержка более сложных разрешений с использованием Spring Expression Language (SpEL)
    • применение

      @Configuration
      public class ProjectConfig extends WebSecurityConfigurerAdapter {
      	@Override
      	protected void configure(HttpSecurity http) throws Exception {
      		http.httpBasic();
      		String expression = "hasAuthority('read') and !hasAuthority('delete')";
      		http.authorizeRequests()
      			.anyRequest()		  // 端点限制
      			.access(expression);  // 权限限制
      	}
      }
      

4.2.2 На основе ролей пользователей

Есть три способа

  • hasRole(String role)

    • Функции

      • Только пользователи с ролевой ролью могут вызывать терминал
    • применение

      @Configuration
      public class ProjectConfig extends WebSecurityConfigurerAdapter {
      	// Omitted code
      	@Override
      	protected void configure(HttpSecurity http) throws Exception {
      		http.httpBasic();
      		http.authorizeRequests()
      			.anyRequest()		 // 端点限制
      			.hasRole("ADMIN");   // 权限限制
      	}
      }
      
  • hasAnyRole(String... roles)

    • Функции
      • У пользователя есть хотя бы одно разрешение в ролях на вызов терминала
  • access()

    • Функции
      • Гибкая поддержка более сложных ролей с использованием Spring Expression Language (SpEL)

4.2.3 Разрешить все

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	// Omitted code
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic();
		http.authorizeRequests()
			.anyRequest()  // 端点限制
			.permitAll();  // 权限限制
	}
}

4.2.4 Запретить все

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	// Omitted code
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic();
		http.authorizeRequests()
			.anyRequest() // 端点限制
			.denyAll();	  // 权限限制
	}
}

4.2.5 Разрешить аутентифицированных пользователей

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	// Omitted code
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic();
		http.authorizeRequests()
			.anyRequest() 	   // 端点限制
			.authenticated();  // 权限限制
	}
}

4.3 Ограничения интерфейса

MVC matchers

  • Функции

    • Интерфейсы можно выбирать с помощью выражений пути MVC.
    • mvcMatchers("/hello") эквивалентно mvcMatchers("/hello") и mvcMatchers("/hello/") , mvc автоматически защитит /hello/
    • Если в пути есть параметры, то есть только один параметр, который может использовать регулярные выражения.
  • метод

    • mvcMatchers (метод HttpMethod, шаблоны String...) Необходимо указать метод HTTP
    • mvcMatchers (строка... шаблоны) Все методы HTTP применяются автоматически
  • применение

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	// Omitted code
    
    //  该应用的接口定义
    //	/a using the HTTP method GET
    //	/a using the HTTP method POST
    //	/a/b using the HTTP method GET
    //	/a/b/c using the HTTP method GET
    //  /a/b/c/d/{param} using the HTTP method GET
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    	http.httpBasic();
    	http.authorizeRequests()
    		// 如果要访问使用了 GET 方法的接口 /a ,用户需要先验证才能访问
    		.mvcMatchers(HttpMethod.GET, "/a")
    		.authenticated()
    		
    		// 所有人都可以访问使用了 POST 方法的接口 /a
    		.mvcMatchers(HttpMethod.POST, "/a")
    		.permitAll()
    		
    		// 以 /a/ 开头的接口都可以访问,* 代表单个路径名
    		.mvcMatchers( "/a/*")
    		.permitAll()
    		
    		// 以 /a/b/ 开头的接口需要验证后才能访问,** 代表匹配任意数量的路径名
    		.mvcMatchers( "/a/b/**")
    		.authenticated()
    		
    		// 如果路径中有参数,参数可以使用正则表达式
    		.mvcMatchers("/a/b/c/d/{param:^[0-9]*$}")
    		.permitAll()
    		
    		// 其余的所有接口都一律不允许被访问
    		.anyRequest()
    		.denyAll();
    	
    	// 因为还没设置 csrf,所以先关闭它,不然会影响 POST 请求
    	http.csrf().disable();
    	}
    }
    

Ant matchers

  • Функции
    • Используйте выражения Ant в качестве путей для выбора интерфейсов
    • Использование такое же, как и у MVC-сопоставителей.
    • mvcMatchers("/hello") эквивалентен только mvcMatchers("/hello"), он не будет автоматически защищать /hello/
    • Если в пути есть параметры, то есть только один параметр, который может использовать регулярные выражения.
  • метод
    • antMatchers(HttpMethod method, String patterns)
    • antMatchers (строковые шаблоны) Все методы HTTP применяются автоматически
    • antMatchers (метод HttpMethod) Равен antMatchers(httpMethod, "/**")

regex matchers

  • Функции

    • Используйте регулярные выражения (регулярные выражения) в качестве путей для выбора интерфейсов.
  • метод

    • regexMatchers(HttpMethod method, String regex)
    • regexMatchers (строковое регулярное выражение) Все методы HTTP применяются автоматически
  • применение

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    // 省略代码
    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.httpBasic();
    		http.authorizeRequests()
    			.regexMatchers(".*/(us|uk|ca)+/(en|fr).*")
    			.authenticated()
    			.anyRequest()
    			.hasAuthority("premium");
    	}
    }
    

🌊5 Внедрить фильтр

5.1 Реализация класса Filter

5.1.1 Реализация интерфейса фильтра

public class MyFilter implements Filter {
	@Override
	public void doFilter(
	ServletRequest servletRequest,
	ServletResponse servletResponse,
	FilterChain filterChain)
		throws IOException, ServletException {
			// 写上你想要的操作
			
			// 将过滤器加入过滤链中
			filterChain.doFilter(request, response);
		}
}

5.1.2 Наследование OncePerRequestFilter

Когда вы подклассифицируете существующие классы фильтров в Spring Security, у вас могут быть некоторые полезные функции, которые максимально упрощают ваш код.

Функции

  • Spring Security не может гарантировать, что фильтры в цепочке фильтров будут вызываться только один раз в одном запросе, и этот класс фильтров может гарантировать, что он будет вызываться только один раз в одном запросе.
  • CorsFilter — его подкласс (CorsFilter будет описан в следующем разделе)

метод

  • void doFilter (запрос ServletRequest, ответ ServletResponse, FilterChain filterChain) Используется для реализации логики фильтрации
  • protected void doFilterInternal (запрос HttpServletRequest, ответ HttpServletResponse, FilterChain filterChain) То же, что и определение doFilter, но этот метод гарантирует, что один запрос в цепочке фильтров вызывается только один раз.
  • защищенное логическое значение shouldNotFilter (запрос HttpServletRequest) Вы можете решить, использовать ли этот фильтр для фильтрации этого запроса для определенного запроса. По умолчанию этот фильтр будет фильтровать все запросы.
  • защищенное логическое значение shouldNotFilterAsyncDispatch() По умолчанию этот фильтр не будет фильтровать асинхронные запросы.
  • защищенное логическое значение shouldNotFilterErrorDispatch() По умолчанию этот фильтр не фильтрует неправильно запланированные запросы.

применение

public class MyFilter extends OncePerRequestFilter {

	// 仅支持 HTTP 请求
	@Override
	protected void doFilterInternal(
		HttpServletRequest request,
		HttpServletResponse response,
		FilterChain filterChain)
			throws ServletException, IOException {
				// 这里写你要实现的逻辑
				filterChain.doFilter(request, response);
			}
}

5.2 Добавьте свой собственный экземпляр класса

5.2.1 Добавить пользовательский фильтр перед целевым фильтром

Метод addFilterBefore()

  • параметр

    • Параметр 1: созданный вами фильтр, который вы хотите добавить в цепочку фильтров.
    • Второй параметр: объект класса целевого фильтра.
  • использовать

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.addFilterBefore(
    				new MyFilter(),
    				BasicAuthenticationFilter.class)
    			.authorizeRequests()
    			.anyRequest().permitAll();
    	}
    }
    

5.2.2 Добавить пользовательский фильтр после целевого фильтра

Метод http.addFilterAfter()

  • параметр

    • Параметр 1: созданный вами фильтр, который вы хотите добавить в цепочку фильтров.
    • Второй параметр: объект класса целевого фильтра.
  • использовать

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.addFilterAfter(
    				new MyFilter(),
    				BasicAuthenticationFilter.class)
    			.authorizeRequests()
    			.anyRequest().permitAll();
    	}
    }
    

5.2.3 Добавьте пользовательский фильтр на место целевого фильтра

Метод http.addFilterAt()

  • Функции

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

    • Параметр 1: созданный вами фильтр, который вы хотите добавить в цепочку фильтров.
    • Второй параметр: объект класса целевого фильтра.
  • использовать

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.addFilterAt(
    				new MyFilter(),
    				BasicAuthenticationFilter.class)
    			.authorizeRequests()
    			.anyRequest().permitAll();
    	}
    }
    

🌊6 Настройка CSRF и CORS

6.1 CSRF

// Из-за ограничения по количеству символов последующие дополнения

6.2 CORS

6.2.1 Что такое КОРС

  • По умолчанию браузеры не разрешают запросы к адресам из источников, отличных от загруженного в данный момент веб-сайта, и браузеры могут использовать механизм CORS, чтобы ослабить эту строгую политику и в некоторых случаях разрешить запросы между разными источниками.
  • image-20211115174729618.png

6.2.2 Как работает CORS

Механизм CORS основан на заголовках HTTP-запросов.

image-20211115211139424.png

Ниже приведены наиболее важные заголовки запроса.

  • Access-Control-Allow-Origin Укажите внешние домены (источники), которые могут получить доступ к ресурсам в вашем домене.
  • Access-Control-Allow-Methods В случае, если вы хотите разрешить доступ к разным доменам, укажите методы HTTP, которые могут получить доступ к вашему домену, например, разрешить только методы HTTP GET для вызова интерфейса.
  • Access-Control-Allow-Headers Добавьте ограничения на то, какие заголовки запросов вы можете использовать в конкретном запросе.

6.2.3 Настройка CORS

Аннотировать с помощью @CrossOrigin

  • Функции

    • Аннотация находится на методе интерфейса
  • применение

    @PostMapping("/test")
    @ResponseBody
    @CrossOrigin("http://localhost:8080")
    // @CrossOrigin({"example.com", "example.org"}) // 可以设置多个来源
    // 可以用 @CrossOrigin 的 allowedHeaders 属性设置请求头
    // 可以用 @CrossOrigin 的 methods 属性设置请求方法
    public String test() {
    	logger.info("Test method called");
    	return "HELLO";
    }
    

Использование CorsConfigurer

  • Функции

    • В классе конфигурации вы можете установить CORS всего приложения
  • применение

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		// 调用 cors() 方法设置 CORS
    		http.cors(c -> {
    			// CorsConfigurationSource 为 HTTP 请求返回 CorsConfiguration
    			CorsConfigurationSource source = request -> {
    				// CorsConfiguration 指明允许哪些来源、请求方法和请求头
    				CorsConfiguration config = new CorsConfiguration();
    				// 至少要指明来源和请求方法,否则会拒绝请求
    				config.setAllowedOrigins(List.of("example.com", "example.org"));
    				config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    				return config;
    			};
    			c.configurationSource(source);
    		});
    		
    	http.csrf().disable();
    	http.authorizeRequests()
    		.anyRequest()
    		.permitAll();
    	}
    }
    

------ Поэтапный бой один ------

Подробный код загрузки, откройте в нем папку ssia-ch11

🌊7 Интеграция OAuth 2

7.1 Что такое

HTTP-протокол

7.2 Принцип

7.2.1 Члены

image-20211120185052006.png

protected resource

  • защищенный ресурс доступен через HTTP-сервер
  • Необходимо проверить полученный токен и определить, следует ли и как обслуживать запрос.
  • В архитектуре OAuth за защищенным ресурсом остается последнее слово, принимать токен или нет.

resource owner

  • владелец ресурса — это сущность, имеющая полномочия делегировать доступ клиенту.
  • В отличие от остальной части системы OAuth, владельцем ресурса является не часть программного обеспечения, а тот, кто использует клиентское программное обеспечение для доступа к чему-то, что они контролируют.
  • Владелец ресурса использует веб-браузер для взаимодействия с сервером авторизации (браузер обычно используется в качестве прокси пользователя)
  • Владелец ресурса также может взаимодействовать с клиентом с помощью веб-браузера.

client

  • Клиент — это программное обеспечение, которое пытается получить доступ к защищенному ресурсу от имени владельца ресурса, оно использует OAuth для получения этого доступа.
  • Ему не нужно анализировать информацию о токене, он просто обрабатывает токен как непрозрачную строку.
  • Клиент может быть веб-приложением, локальной программой или программой JavaScript, настроенной в браузере.

authorization server

  • сервер авторизации — это HTTP-сервер, выступающий в роли центрального компонента системы OAuth.
  • Сервер авторизации аутентифицирует владельца ресурса и клиента, предоставляет механизм, позволяющий владельцу ресурса авторизовать клиента, и выдает токен клиенту.

7.2.2 Компоненты

access tokens

  • Генерируется сервером авторизации
  • Сочетание прав доступа, запрошенных от имени клиента, владельца ресурса, авторизующего клиента, и прав, предоставленных во время этой авторизации.
  • жетоны непрозрачны

scopes

  • области действия представляют собой набор разрешений для защищенных ресурсов, определенных защищенным ресурсом
  • клиент может запрашивать определенные области
  • Сервер авторизации может разрешить владельцу ресурса предоставлять или отклонять определенные области для данного клиента во время его запроса.

refresh tokens

  • Генерируется сервером авторизации
  • жетоны непрозрачны
  • токены обновления позволяют клиенту сузить свою область доступа, например, если клиенту предоставлены области A, B и C, но он знает, что ему нужна только область A для выполнения определенного вызова, он может использовать токены обновления для запроса областей, которые применяются только к токенам доступа для области A
  • Токены доступа в OAuth 2.0 могут устанавливать время истечения срока действия.По истечении срока действия токенов доступа клиент использует токены обновления для запроса новых токенов доступа с сервера авторизации (без прохождения через владельца ресурса), поэтому токены обновления никогда не будут отправлены на защищенный сервер. ресурс.

image-20211119161219137.png

authorization grants

  • Это метод предоставления клиенту доступа к серверу ресурсов с использованием протокола OAuth. В случае успеха клиент в конечном итоге получит токен.

7.2.3 Взаимодействие между элементами и компонентами

  • back channel

    Два компонента обмениваются данными через HTTP без участия браузера.

image-20211119165543556.png

  • front channel

    Фронт-канальная связь — это метод непрямой связи между двумя системами с использованием HTTP-запросов через промежуточный веб-браузер.

image-20211119165625977.png

7.2.4 Типы предоставления авторизации

тип предоставления кода аутентификации (наиболее распространенный)

Код проверки подлинности — это временные учетные данные, используемые для представления полномочий владельца ресурса клиенту.

Snipaste_2021-11-19_13-54-59.png

  1. Владелец ресурса инициирует запрос к защищенному ресурсу через клиента

  2. Когда клиенту нужен токен доступа, отправьте владельцу ресурса на сервер авторизации запрос

image-20211121105120013.png

  • Ответ от клиента следующий (соответствует процессу 2.1 общей схемы)

    HTTP/1.1 302 Moved Temporarily
    x-powered-by: Express
    Location: http://localhost:9001/authorize?response_type=code&scope=foo&client
    _id=oauth-client-1&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&
    state=Lwt50DDQKUB8U7jtfLQCVGDL9cnmwHH1
    Vary: Accept
    Content-Type: text/html; charset=utf-8
    Content-Length: 444
    Date: Fri, 31 Jul 2015 20:50:19 GMT
    Connection: keep-alive
    
  • Перенаправление в браузер приведет к тому, что браузер отправит HTTP GET на сервер авторизации (соответствует процессу 2.2 на общей схеме)

    GET /authorize?response_type=code&scope=foo&client_id=oauth-client
    -1&redirect_uri=http%3A%2F%2Flocalhost%3A9000%
    2Fcallback&state=Lwt50DDQKUB8U7jtfLQCVGDL9cnmwHH1 HTTP/1.1
    Host: localhost:9001
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0)
    Gecko/20100101 Firefox/39.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Referer: http://localhost:9000/
    Connection: keep-alive
    
  1. Владелец ресурса проходит аутентификацию непосредственно на сервере авторизации, а не через клиента, что защищает пользователей от необходимости делиться своими учетными данными с клиентом.

    OAuth не указывает технологию аутентификации, поэтому вы можете выбрать свою собственную (эта статья посвящена использованию Spring Security для реализации аутентификации).

image-20211121105100375.png

  1. Владелец ресурса решает делегировать часть своих полномочий клиенту, сервер авторизации имеет множество различных вариантов для выполнения этой работы.

image-20211121111321979.png

  • На следующем рисунке показан процесс фактического делегирования разрешений клиенту.

image-20211121113108408.png

  • Многие серверы авторизации позволяют хранить это решение об авторизации для использования в будущем, и будущие запросы на тот же доступ от того же клиента не будут запрашивать пользователя в интерактивном режиме (как на картинке выше).
  • Сервер авторизации может даже отменять решения конечных пользователей на основе внутренних политик, таких как белые или черные списки клиентов.
  1. Сервер авторизации перенаправляет пользователя обратно к клиенту

image-20211121114822854.png

  • Сервер авторизации перенаправляет пользовательский агент обратно клиенту (см. рис. 5.1). Код здесь представляет собой одноразовые учетные данные, называемые кодом авторизации, которые представляют собой результат решения пользователя об авторизации.

    HTTP 302 Found
    Location: http://localhost:9000/oauth_callback?code=8V1pr0rJ&state=Lwt50DDQKU
    B8U7jtfLQCVGDL9cnmwHH1
    
  • Браузер отправляет клиенту следующий запрос (как показано на рис. 5.2):

    GET /callback?code=8V1pr0rJ&state=Lwt50DDQKUB8U7jtfLQCVGDL9cnmwHH1 HTTP/1.1
    Host: localhost:9000
    
  1. Клиент отправляет полученный код и другую информацию (как показано ниже) на конечную точку токена сервера авторизации.

    POST /token
    Host: localhost:9001
    Accept: application/json
    Content-type: application/x-www-form-encoded
    Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x
    grant_type=authorization_code&
    redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&code=8V1pr0rJ
    

image-20211121161922886.png

  1. Сервер авторизации проверяет действительность учетных данных клиента (информация в начале авторизации на предыдущем шаге), затем проверяет действительность кода (информация о коде на предыдущем шаге) и гарантирует, что клиент, делающий этот запрос, такой же, как клиент, который сделал исходный запрос. Если оба пройдены, верните токен доступа клиенту

image-20211121162814138.png

  • Этот токен возвращается в ответе HTTP как объект JSON.

    HTTP 200 OK
    Date: Fri, 31 Jul 2015 21:19:03 GMT
    Content-type: application/json
    {
    	“access_token”: “987tghjkiu6trfghjuytrghj”,
    	“token_type”: “Bearer”
    }
    
  • Этот HTTP-ответ также может включать токен обновления и дополнительную информацию (например, области действия токена и время истечения срока его действия).

  1. Клиент использует токен доступа для доступа к защищенному ресурсу.

image-20211121174031980.png

password grant type

Snipaste_2021-11-21_17-46-30.png

  1. Клиент собирает логин и пароль владельца ресурса

  2. Отправить запрос на сервер авторизации для получения токена доступа

    POST /token
    Host: localhost:9001
    Accept: application/json
    Content-type: application/x-www-form-encoded
    Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x
    grant_type=password&scope=foo%20bar&username=alice&password=secret
    
  3. Наконец, используйте токен доступа для доступа к защищенному ресурсу.

client credentials grant type

image-20211121180419443.png

  1. Для получения токена доступа клиент отправляет запрос на сервер авторизации

    POST /token
    Host: localhost:9001
    Accept: application/json
    Content-type: application/x-www-form-encoded
    Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x
    grant_type=client_credentials&scope=foo%20bar
    
  2. Наконец, используйте токен доступа для доступа к защищенному ресурсу.

Как правильно выбрать типы предоставления авторизации

image-20211119154517430.png

7.3 Создание клиента

7.3.1 Зависимости

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

7.3.2 Установка класса конфигурации

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 将 OAuth2LoginAuthenticationFilter 加入过滤链中
		http.oauth2Login();
		
		http.authorizeRequests()
		.anyRequest()
		.authenticated();
	}
}

7.3.3 Реализация регистрации клиентов

Что такое регистрация клиентов

ClientRegistration представляет клиента, включая идентификатор и ключ клиента, режим авторизации, URI перенаправления, области действия и другую информацию.

Создайте экземпляр ClientRegistration

Предоставьте следующие два способа

  • создать объект

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	private ClientRegistration clientRegistration() {
    		ClientRegistration cr = ClientRegistration.withRegistrationId("github")
    			.clientId("a7553955a0c534ec5e6b")
    			.clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
    			.scope(new String[]{"read:user"})
    			.authorizationUri("https://github.com/login/oauth/authorize")
    			.tokenUri("https://github.com/login/oauth/access_token")
    			.userInfoUri("https://api.github.com/user")
    			.userNameAttributeName("id")
    			.clientName("GitHub")
    			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    			.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
    			.build();
    		return cr;
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.oauth2Login();
    	
    		http.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    	}
    }
    
  • Использование CommonOAuth2Provider CommonOAuth2Provider предоставляет общие конструкторы клиентов с предустановленными разумными значениями по умолчанию, обычно используемые (FACEBOOK, GITHUB, GOOGLE, OKTA).

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	private ClientRegistration clientRegistration() {
    		return CommonOAuth2Provider.GITHUB
    								.getBuilder("github")
    								.clientId("a7553955a0c534ec5e6b")
    								.clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
    								.build();
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.oauth2Login();
    	
    		http.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    	}
    }
    

7.3.4 Реализация ClientRegistrationRepository

Что такое ClientRegistrationRepository

Используется для поиска ClientRegistration по регистрационному идентификатору

Метод реализации

  • Внедрить как @Bean

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Bean
    	public ClientRegistrationRepository clientRepository() {
    		var c = clientRegistration();
    		// InMemoryClientRegistrationRepository 是 ClientRegistrationRepository 的子类
    		return new InMemoryClientRegistrationRepository(c);
    	}
    	
    	private ClientRegistration clientRegistration() {
    		return CommonOAuth2Provider.GITHUB.getBuilder("github")
    								.clientId("a7553955a0c534ec5e6b")
    								.clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
    								.build();
    	}
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.oauth2Login();
    		http.authorizeRequests()
    			.anyRequest().authenticated();
    	}
    }
    
  • Настроить с помощью настройщика

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.oauth2Login(c -> {
    			c.clientRegistrationRepository(clientRepository());
    		});
    		
    		http.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    	}
    	
    	private ClientRegistrationRepository clientRepository() {
    		var c = clientRegistration();
    		return new InMemoryClientRegistrationRepository(c);
    	}
    	
    	private ClientRegistration clientRegistration() {
    		return CommonOAuth2Provider.GITHUB.getBuilder("github")
    								.clientId("a7553955a0c534ec5e6b")
    								.clientSecret("1795b30b425ebb79e424afa51913f1c724da0dbb")
    								.build();
    	}
    }
    

7.3.5 Упрощение настройки с помощью application.properties

Этот метод не требует реализации ClientRegistration и ClientRegistrationRepository, Spring Boot автоматически настроит его для вас на основе application.properties.

  • файл application.properties Есть два случая

    • Использовать общий клиент

      spring.security.oauth2.client.registration.github.client-id=a7553955a0c534ec5e6b
      spring.security.oauth2.client.registration.github.client-secret=1795b30b425ebb79e424afa51913f1c724da0dbb
      
    • Используйте пользовательский клиент Нам нужно указать детали сервера авторизации, используя группу свойств, начинающуюся с spring.security.oauth2.client.provider.

      spring.security.oauth2.client.provider.myprovider.authorization-uri=<some uri>
      spring.security.oauth2.client.provider.myprovider.token-uri=<some uri>
      
  • класс конфигурации

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.oauth2Login();
    		
    		http.authorizeRequests()
    			.anyRequest()
    			.authenticated();
    	}
    }
    

7.4 Сервер авторизации сборки

7.4.1 Управление пользователями

@Configuration
// 继承 WebSecurityConfigurerAdapter 以访问 AuthenticationManager 实例
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	public UserDetailsService uds() {
		var uds = new InMemoryUserDetailsManager();
		var u = User.withUsername("john")
		.password("12345")
		.authorities("read")
		.build();
		uds.createUser(u);
		return uds;
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return NoOpPasswordEncoder.getInstance();
	}
	
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	
	// 配置 form-login authentication
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.formLogin();
	}
}

7.4.2 Два метода развертывания

Сервер авторизации и сервер ресурсов должны быть развернуты одинаково.

1 жетон удаленной проверки

Сервер ресурсов и Защищенный ресурс на рисунке означают одно и то же

Snipaste_2021-11-18_14-55-37.png

  • полагаться

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <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>
    
    <dependencyManagement>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-dependencies</artifactId>
    			<version>Hoxton.SR1</version>
    			<type>pom</type>
    			<scope>import</scope>
    		</dependency>
    	</dependencies>
    </dependencyManagement>
    
  • установить класс конфигурации

    @Configuration
    @EnableAuthorizationServer
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    	// 注入 AuthenticationManager
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    		// 设置 AuthenticationManager
    		endpoints.authenticationManager(authenticationManager);
    	}
    	
    	// 方法一:使用 InMemoryClientDetailsService 配置 client
    //	@Override
    //	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //		// 创建 ClientDetailsService 实例
    //		var service = new InMemoryClientDetailsService();
    //		// 创建 ClientDetails 实例
    //		var cd = new BaseClientDetails();
    //		cd.setClientId("client");
    //		cd.setClientSecret("secret");
    //		cd.setScope(List.of("read"));
    //		cd.setAuthorizedGrantTypes(List.of("password"));
    //		// 将 ClientDetails 实例添加到 InMemoryClientDetailsService
    //		service.setClientDetailsStore(Map.of("client", cd));
    //		// 配置 ClientDetailsService 以供我们的 authorization server 使用
    //		clients.withClientDetails(service);
    //	}
    	
    	// 方法二:在内存中配置 ClientDetails
    	@Override
    	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    		clients.inMemory()
    			.withClient("client")
    			.secret("secret")
    			// 配置 password grant type
    			.authorizedGrantTypes("password")
    			.scopes("read");
    		
    		// 如果有多个 client,为了安全,请让每个 client 的名称和密码都不相同
    		clients.inMemory()
    			.withClient("client1")
    			.secret("secret")
    			// 配置 authorization code grant type
    			.authorizedGrantTypes("authorization_code")
    			.scopes("read")
    			.redirectUris("http://localhost:9090/home")
    		.and()
    			.withClient("client2")
    			.secret("secret2")
    			// 一个 client 可配置多个授权方式
    			.authorizedGrantTypes("authorization_code", "password", "refresh_token")
    			.scopes("read")
    			.redirectUris("http://localhost:9090/home");
    	}
    	
    	// 指定我们可以调用 check_token 端点的条件
    	public void configure(AuthorizationServerSecurityConfigurer security) {
    		// 只有验证成功后才能调用 check_token 端点(这个端点是给 Resource server 调用的)
    		security.checkTokenAccess("isAuthenticated()");
    	}
    }
    

2 Сервер авторизации и сервер ресурсов совместно используют базу данных

Snipaste_2021-11-18_14-56-29.png

Здесь сервер авторизации, ресурсный сервер и база данных размещены на одном хосте

  • полагаться

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <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-jdbc</artifactId>
    </dependency>
    <dependency>
    	<groupId>mysql</groupId>
    	<artifactId>mysql-connector-java</artifactId>
    </dependency>
    
  • создать базу данных Сервер ресурсов не нужно создавать позже, потому что он является общим.

    CREATE TABLE IF NOT EXISTS `oauth_access_token` (
    	`token_id` varchar(255) NOT NULL,
    	`token` blob,
    	`authentication_id` varchar(255) DEFAULT NULL,
    	`user_name` varchar(255) DEFAULT NULL,
    	`client_id` varchar(255) DEFAULT NULL,
    	`authentication` blob,
    	`refresh_token` varchar(255) DEFAULT NULL,
    	PRIMARY KEY (`token_id`));
    	
    CREATE TABLE IF NOT EXISTS `oauth_refresh_token` (
    	`token_id` varchar(255) NOT NULL,
    	`token` blob,
    	`authentication` blob,
    	PRIMARY KEY (`token_id`));
    
  • Настройте источник данных в application.properties

    spring.datasource.url=jdbc:mysql://localhost/spring?useLegacyDatetimeCode=false&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=
    spring.datasource.initialization-mode=always
    
  • установить класс конфигурации

    @Configuration
    @EnableAuthorizationServer
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    	@Autowired
    	private AuthenticationManager authenticationManager;
    	
    	// 注入我们在 application.properties 中配置的数据源
    	@Autowired
    	private DataSource dataSource;
    	
    	@Override
    	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    		clients.inMemory()
    			.withClient("client")
    			.secret("secret")
    			.authorizedGrantTypes("password", "refresh_token")
    			.scopes("read");
    	}
    	
    	@Override
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    		endpoints.authenticationManager(authenticationManager)
    				// 配置 tokenStore
    				.tokenStore(tokenStore());
    	}
    	
    	// 创建一个 JdbcTokenStore 实例,通过 application.properties 文件中配置的数据源提供对数据库的访问
    	@Bean
    	public TokenStore tokenStore() {
    		return new JdbcTokenStore(dataSource);
    	}
    }
    

7.5 Сервер ресурсов сборки

7.5.1 Два метода развертывания

1 жетон удаленной проверки

Snipaste_2021-11-18_14-55-37.png

  • полагаться

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <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>
    
    <dependencyManagement>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-dependencies</artifactId>
    			<version>Hoxton.SR1</version>
    			<type>pom</type>
    			<scope>import</scope>
    		</dependency>
    	</dependencies>
    </dependencyManagement>
    
  • Добавьте учетные данные на сервер ресурсов

    @Configuration
    @EnableAuthorizationServer
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    	// Omitted code
    	@Override
    	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    		clients.inMemory()
    			.withClient("client")
    			.secret("secret")
    			.authorizedGrantTypes("password", "refresh_token")
    			.scopes("read")
    		.and()
    			.withClient("resourceserver")
    			.secret("resourceserversecret");
    	}
    }
    

2 Сервер авторизации и сервер ресурсов совместно используют базу данных

Snipaste_2021-11-18_14-56-29.png

  • полагаться

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <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-jdbc</artifactId>
    </dependency>
    <dependency>
    	<groupId>mysql</groupId>
    	<artifactId>mysql-connector-java</artifactId>
    </dependency>
    
  • Настроить application.properties

    server.port=9090
    spring.datasource.url=jdbc:mysql://localhost/spring
    spring.datasource.username=root
    spring.datasource.password=
    
  • установить класс конфигурации

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    	// 注入我们在 application.properties 中配置的数据源
    	@Autowired
    	private DataSource dataSource;
    	
    	@Override
    	public void configure(ResourceServerSecurityConfigurer resources) {
    		// 配置 tokenStore
    		resources.tokenStore(tokenStore());
    	}
    	
    	// 创建基于已注入的数据源的 JdbcTokenStore 实例
    	@Bean
    	public TokenStore tokenStore() {
    		return new JdbcTokenStore(dataSource);
    	}
    }
    

🌊8 Интегрируйте JWT и криптографические подписи

Эта интеграция осуществляется на основе интеграции OAuth2.

8.1 JWT

8.1.1 Что такое JWT

JWT, или JSON Web Token, — это упрощенная спецификация авторизации и аутентификации в стиле JSON, которая обеспечивает распределенную авторизацию веб-приложений без сохранения состояния.

8.1.2 Особенности

  • JWT состоит из трех частей: заголовок и тело представлены в формате JSON, и они закодированы в Base64, а третья часть — это подпись, сгенерированная с использованием алгоритма шифрования, использующего заголовок и тело в качестве входных данных.

Snipaste_2021-11-23_15-35-49.png

  • Когда JWT подписан, мы называем этоJWS ( JSON Web Token Signed )

  • Если toekn зашифрован, мы также называем егоJWE ( JSON Web Token Encrypted )

8.2 Подписание токенов с использованием алгоритма симметричного шифрования

Просто подпишите токен, а не шифруйте весь токен

image-20211123170706725.png

8.2.1 Обязанности

image-20211125221625227.png

8.2.2 Реализация сервера авторизации

Здесь сервер авторизации и сервер ресурсов используют один и тот же ключ шифрования.

полагаться

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

установить класс конфигурации

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	// 从 application.properties 文件中获取对称密钥的值
	@Value("${jwt.key}")
	private String jwtKey;
	
	@Autowired
	private AuthenticationManager authenticationManager;
	
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.inMemory()
			.withClient("client")
			.secret("secret")
			.authorizedGrantTypes("password", "refresh_token")
			.scopes("read");
	}
	
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
		endpoints.authenticationManager(authenticationManager)
				// 配置 tokenStore 和 accessTokenConverter
				.tokenStore(tokenStore())
				.accessTokenConverter(jwtAccessTokenConverter());
	}
	
	@Bean
	public TokenStore tokenStore() {
		// 使用与之关联的 AccessTokenConverter 创建 TokenStore
		return new JwtTokenStore(jwtAccessTokenConverter());
	}
	
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		var converter = new JwtAccessTokenConverter();
		// 设置对称密钥
		converter.setSigningKey(jwtKey);
		return converter;
	}
}

Сохраните симметричный ключ в application.properties (не рекомендуется помещать сюда конфиденциальные данные во время фактической разработки, поскольку это небезопасно)

jwt.key=MjWP5L7CiD

Настроить управление пользователями

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	public UserDetailsService uds() {
		var uds = new InMemoryUserDetailsManager();
		var u = User.withUsername("john")
					.password("12345")
					.authorities("read")
					.build();
		uds.createUser(u);
		return uds;
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return NoOpPasswordEncoder.getInstance();
	}
	
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}

8.2.3 Реализация сервера ресурсов

и единственное, что меняется в конфигурации с использованием симметричного шифрования, — это определение объекта JwtAccessToken-Converter.

полагаться

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<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>

Изменить класс конфигурации

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	// 从 application.properties 注入密钥值
	@Value("${jwt.key}")
	private String jwtKey;
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		// 配置 tokenStore
		resources.tokenStore(tokenStore());
	}
	
	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(jwtAccessTokenConverter());
	}
	
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		var converter = new JwtAccessTokenConverter();
		converter.setSigningKey(jwtKey);
		return converter;
	}
}

Изменить application.properties

Сохраните симметричный ключ в application.properties (не рекомендуется помещать сюда конфиденциальные данные во время фактической разработки, поскольку это небезопасно)

jwt.key=MjWP5L7CiD

8.3 Подписание токенов с использованием алгоритма асимметричного шифрования

image-20211123171423889.png

8.3.1 Обязанности

image-20211126112125340.png

8.3.2 Что такое асимметричная пара ключей

Пара асимметричных ключей содержит два ключа, один из которых является закрытым ключом, который используется для подписи токена, а другой — открытым ключом, который используется для проверки подписи.

8.3.3 Генерация ключей

Вы можете использовать keytool или OpenSSL для создания пары асимметричных ключей (используйте keytool ниже, потому что JDK имеет этот инструмент)

сгенерировать закрытый ключ

# 文件名为 ssia.jks
# 密码为 ssia123,这个密码用来保护 private key
# 别名为 ssia
# 密钥生成算法为 RSA
keytool -genkeypair -alias ssia -keyalg RSA -keypass ssia123 -keystore ssia.jks -storepass ssia123

сгенерировать открытый ключ

keytool -list -rfc --keystore ssia.jks | openssl x509 -inform pem -pubkey
# 输入以上信息后会有提示输入密码,这时填 private key 的密码 ssia123
# 输完密码后会显示以下 public key 信息
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijLqDcBHwtnsBw+WFSzG
VkjtCbO6NwKlYjS2PxE114XWf9H2j0dWmBu7NK+lV/JqpiOi0GzaLYYf4XtCJxTQ
DD2CeDUKczcd+fpnppripN5jRzhASJpr+ndj8431iAG/rvXrmZt3jLD3v6nwLDxz
pJGmVWzcV/OBXQZkd1LHOK5LEG0YCQ0jAU3ON7OZAnFn/DMJyDCky994UtaAYyAJ
7mr7IO1uHQxsBg7SiQGpApgDEK3Ty8gaFuafnExsYD+aqua1Ese+pluYnQxuxkk2
Ycsp48qtUv1TWp+TH3kooTM6eKcnpSweaYDvHd/ucNg8UDNpIqynM1eS7KpffKQm
DwIDAQAB
-----END PUBLIC KEY-----

8.3.4 Реализация сервера авторизации

полагаться

То же, что и 8.2.2 зависимости

Изменить application.properties

Не рекомендуется размещать здесь конфиденциальные данные во время фактической разработки, потому что это небезопасно.

# 用来设置 JwtTokenStore
password=ssia123
privateKey=ssia.jks
alias=ssia

Изменить класс конфигурации

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	// 注入 private key 密码
	@Value("${password}")
	private String password;
		
	// 注入 private key 类路径
	@Value("${privateKey}")
	private String privateKey;
		
	// 注入 private key 名称
	@Value("${alias}")
	private String alias;
		
	@Autowired
	private AuthenticationManager authenticationManager;
			
	// Omitted code
		
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		var converter = new JwtAccessTokenConverter();
		// 从类路径读取 private key
		KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
				new ClassPathResource(privateKey),
				password.toCharArray()
				);
		// 使用 KeyStoreKeyFactory 对象检索密钥对并将密钥对设置到 JwtAccessTokenConverter 对象
		converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
		return converter;
	}
}

8.3.5 Реализация сервера ресурсов

Сервер ресурсов проверяет подпись токена с помощью открытого ключа.

полагаться

То же, что и 8.2.3 зависимости

Изменить application.properties

server.port=9090
publicKey=publicKey=-----BEGIN PUBLIC KEY-----MIIBIjANBghk太长了省略…-----END PUBLIC KEY-----

Изменить класс конфигурации

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	// 注入 public key
	@Value("${publicKey}")
	private String publicKey;
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		resources.tokenStore(tokenStore());
	}
	
	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(jwtAccessTokenConverter());
	}
	
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		var converter = new JwtAccessTokenConverter();
		// 设置 public key,token store 使用 public key 验证 token
		converter.setVerifierKey(publicKey);
		return converter;
	}
}

8.3.6 Использование конечных точек для предоставления открытых ключей

В предыдущей реализации мы поместили открытый ключ и закрытый ключ на сервер ресурсов и сервер авторизации соответственно.Если пара ключей изменится, ресурсный сервер не сможет проверить токен, поэтому мы должны поместить как открытый ключ, так и закрытый ключ На сервере авторизации используйте конечную точку для раскрытия открытого ключа, чтобы сервер ресурсов мог получить доступ к открытому ключу через конечную точку. Преимущество этого заключается в том, что пару ключей можно периодически менять, что повышает безопасность, не влияя на проверку токена.

image-20211125174526333.png

долг

image-20211126135324114.png

Изменить сервер авторизации

Измените класс конфигурации 8.3.4.

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	// Omitted code
	
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.inMemory()
			.withClient("client")
			.secret("secret")
			.authorizedGrantTypes("password", "refresh_token")
			.scopes("read")
			.and()
			// 添加代表 resource server 的 client 凭证,用于 resource server 来访问
			.withClient("resourceserver")
			.secret("resourceserversecret");
	}
	
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) {
		// authorization server 默认提供了一个暴露 public key 的端点 /oauth/token_key ,但是默认情况下是不允许访问的,需要进行以下设置才能被访问
		// 配置 authorization server 给 public key 暴露端点,只用通过 client 验证的才能访问
		security.tokenKeyAccess("isAuthenticated()");
	}
}

Изменить сервер ресурсов

Изменить application.properties 8.3.5

server.port=9090
security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_key
security.oauth2.client.client-id=resourceserver
security.oauth2.client.client-secret=resourceserversecret

Поскольку сервер ресурсов теперь получает открытый ключ от конечной точки /oauth/token_key, теперь нет необходимости устанавливать класс конфигурации, и класс конфигурации можно оставить пустым.

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}

8.4 Пользовательский JWT

8.4.1 Детали, включенные по умолчанию в JWT

По умолчанию токен обычно хранит все данные, необходимые для базовой авторизации.

{
	"exp": 1582581543,
	"user_name": "john",
	"authorities": [
		"read"
	],
	"jti": "8e208653-79cf-45dd-a702-f6b694b417e7",
	"client_id": "client",
	"scope": [
		"read"
	]
}

8.4.2 Настройка сервера авторизации для добавления пользовательской информации в токен

Реализовать TokenEnhancer

public class CustomTokenEnhancer implements TokenEnhancer {
	@Override
	public OAuth2AccessToken enhance(
			OAuth2AccessToken oAuth2AccessToken,
			OAuth2Authentication oAuth2Authentication) {
		var token = new DefaultOAuth2AccessToken(oAuth2AccessToken);
		// 将我们想添加的信息放进键值对里面,这里我们添加的自定义信息是时区
		Map<String, Object> info = Map.of("generatedInZone", ZoneId.systemDefault().toString());
		token.setAdditionalInformation(info);
		return token;
	}
}

Настройте пользовательский TokenEnhancer на сервере авторизации.

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	// Omitted code
	
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
		TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
		// 将我们自定义的 TokenEnhancer 添加进 list 中
		var tokenEnhancers = List.of(new CustomTokenEnhancer(), jwtAccessTokenConverter());
		// 将 TokenEnhancer 的 List 添加进链中
		tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
		endpoints
			.authenticationManager(authenticationManager)
			.tokenStore(tokenStore())
			.tokenEnhancer(tokenEnhancerChain);
	}
}

Модифицированный токен

{
	"user_name": "john",
	"scope": [
		"read"
	],
	"generatedInZone": "Europe/Bucharest",
	"exp": 1582591525,
	"authorities": [
		"read"
	],
	"jti": "0c39ace4-4991-40a2-80ad-e9fdeb14f9ec",
	"client_id": "client"
}

8.4.3 Настройка сервера ресурсов для чтения пользовательской информации из токена

Наследовать JwtAccessTokenConverter

AccessTokenConverter — это класс, который преобразует токен в аутентификацию.

public class AdditionalClaimsAccessTokenConverter extends JwtAccessTokenConverter {
	@Override
	public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
		var authentication = super.extractAuthentication(map);
		// 往 authentication 添加自定义信息
		authentication.setDetails(map);
		return authentication;
	}
}

Настройте пользовательский JwtAccessTokenConverter на сервер ресурсов.

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	// Omitted code
	
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		var converter = new AdditionalClaimsAccessTokenConverter();
		converter.setVerifierKey(publicKey);
		return converter;
	}
}

Один из способов проверить успешность изменения токена — вернуть пользовательскую добавленную информацию через HTTP-ответ в conreoller.

@RestController
public class HelloController {
	@GetMapping("/hello")
	public String hello(OAuth2Authentication authentication) {
		OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
		return "Hello! " + details.getDecodedDetails();
	}
}

------ Поэтапный бой II ------

Подробный код загрузки, откройте в нем папки ssia-ch18-ex1 и ssia-ch18-ex2

🌊 эпилог

Самое важное, что нужно делать во время учебы, — это читать книги.Хотя статьи в блогах в Интернете полезны, они играют лишь вспомогательную роль.Это похоже на чтение учебников, даже если в прошлом было много полезных учебных пособий. Из-за ограниченных возможностей автора этой статьи, если в этой статье есть какие-либо недостатки и ошибки, пожалуйста, читатели указывают в области комментариев. Наконец, я надеюсь, что эта статья поможет вам, если вы найдете ее полезной, пожалуйста, поставьте лайк, спасибо за просмотр!

🌊 Ссылка

текстовая ссылка

[1]Лауренциу Спилкэ. Spring Security в действии[M].America: Manning, 2020.

[2]Justin Richer, Antonio Sanso.OAuth2 in Action[M].America: Manning, 2017.

[3]Rod Johnson.Spring Security Reference[EB/OL].docs.spring.IO/spring-brush-ECU…, 2020.

[4]Spring Team.Spring Security Source Code[DB]

ссылка на изображение

总集.png