Комбинированная авторизация в Spring Security: Социальные сети и логин через username/password

от автора

Привет! Меня зовут Данекер, я Fullstack-разработчик (Java, Angular). Несмотря на то, что уже работаю в компании, я продолжаю находить время для собственных проектов, через которые изучаю интересующие меня технологии и подходы. В рамках одного из таких проектов я решил разобраться с авторизацией и аутентификацией на основе базы данных в Spring Security 6, а также внедрить авторизацию с помощью социальных сетей (Google, GitHub и другие). В этой версии произошло немало изменений по сравнению с предыдущими. Примеры из документации не всегда полны, а материалов на русском языке по этой теме я почти не нашел. Информацию я собирал по крупицам из различных иностранных источников. Теперь я хочу поделиться с вами тем, что удалось узнать.

Предполагаю, что большинство читателей знакомы с понятиями авторизации и аутентификации, а также с их различиями. Однако, для тех, кто только начинает изучать эту тему, кратко объясню: аутентификация — это процесс проверки личности пользователя, чтобы определить, имеет ли он доступ к ресурсу в целом. Авторизация же — это распределение прав и возможностей для уже аутентифицированных пользователей. Авторизация основывается на ролях и других характеристиках зарегистрированного пользователя, о которых мы поговорим позже.

Основная проблема состоит в том, что начиная с версии Spring Security 5.7.0 класс WebSecurityConfigurerAdapter объявлен устаревшим и его использование в будущих версиях невозможно. Однако большинство существующих руководств все еще опираются на наследование этого класса.

Итак, для начала создадим новый проект и финальный build.gradle(если вы используете maven тогда pom.xml) должен выглядеть таким образом:

plugins {     id 'java'     id 'org.springframework.boot' version '3.2.5'     id 'io.spring.dependency-management' version '1.1.4' }  group = 'kz.danekerscode' version = '0.0.1-SNAPSHOT'  java {     sourceCompatibility = '17' }  configurations {     compileOnly {         extendsFrom annotationProcessor     } }  repositories {     mavenCentral() }  dependencies {     implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'     implementation 'org.springframework.boot:spring-boot-starter-security'      implementation 'org.springframework.boot:spring-boot-starter-data-redis'     implementation 'org.springframework.session:spring-session-data-redis'      implementation 'org.springframework.boot:spring-boot-starter-web'     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'     runtimeOnly 'org.postgresql:postgresql'      compileOnly 'org.projectlombok:lombok'     annotationProcessor 'org.projectlombok:lombok'     testImplementation 'org.springframework.boot:spring-boot-starter-test'     testImplementation 'org.springframework.security:spring-security-test'      testRuntimeOnly 'org.junit.platform:junit-platform-launcher' }  tasks.named('test') {     useJUnitPlatform() } 

Также дополнительные конфигурации в application.yaml

spring:   application:     name: habr-spring-security-6   datasource:     url: jdbc:postgresql://localhost:5432/habr_spring_security_6 # или ссылка для любой другой реляционной базы данных     username: postgres # поменяйте если не совпадает с вашим     password: postgres # это тоже   jpa:     open-in-view: false     hibernate:       ddl-auto: update     show-sql: true     properties:       hibernate:         enable_lazy_load_no_trans: true         format_sql: true   data:     redis:       host: localhost       port: 6379   security:     oauth2:       client:         registration:           github:             provider: github             client-id: ${GITHUB_CLIENT_ID}             client-secret: ${GITHUB_CLIENT_SECRET}             scope:               - user:email               - read:user         provider:           github:             user-name-attribute: login

Для того чтобы получить параметры GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET вам нужно создать OAuth клиента в GitHub. Подробная документация по этой ссылке. Также, не забываем, что redirect-uri должен быть http://localhost:8080/login/oauth2/code/github

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

@Entity @Getter @Setter @Table(name = "users") public class User implements UserDetails {     @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String username;     @Enumerated(EnumType.STRING)     private AuthType authType;     private String email;     private String password;     private String role = "ROLE_USER"; // TODO советуй использовать Enum или же другую сущность      @Override     public Collection<? extends GrantedAuthority> getAuthorities() {         return new HashSet<>(){{             add(new SimpleGrantedAuthority(role));         }};     }      @Override     public boolean isAccountNonExpired() {         return true;     }      @Override     public boolean isAccountNonLocked() {         return true;     }      @Override     public boolean isCredentialsNonExpired() {         return true;     }      @Override     public boolean isEnabled() {         return true;     } }

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

@Component @RequiredArgsConstructor public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {      // Данные поля подходят только github api, если у вас другой провайдер,     // вам следует проверить его документацию     record EmailDetails(String email, Boolean primary, Boolean verified) {     }      private final UserRepository userRepository;     private final OAuth2AuthorizedClientService authorizedClientService;     private final RestClient restClient = RestClient.builder()             .baseUrl("https://api.github.com/user/emails") // другой url если другой провайдер соотвественно             .build(); // лучше получать это значение с ClientRegistration      @Override     public void onAuthenticationSuccess(             HttpServletRequest request,             HttpServletResponse response,             Authentication auth     ) throws IOException {         if (auth instanceof OAuth2AuthenticationToken auth2AuthenticationToken) {             var principal = auth2AuthenticationToken.getPrincipal();             var username = principal.getName();             var email = fetchUserEmailFromGitHubApi(auth2AuthenticationToken.getAuthorizedClientRegistrationId(), username);              if (!userRepository.existsByEmail(email)) {                 var user = new User();                 user.setEmail(email);                 user.setUsername(username);                 userRepository.save(user);             }         }          super.clearAuthenticationAttributes(request);         super.getRedirectStrategy().sendRedirect(request, response, "/api/v1/user/me");     }      private String fetchUserEmailFromGitHubApi(String clientRegistrationId, String principalName) {         var authorizedClient = authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName);         var accessToken = authorizedClient.getAccessToken().getTokenValue();          var userEmailsResponse = restClient.get()                 .headers(headers -> headers.setBearerAuth(accessToken))                 .retrieve()                 .body(EmailDetails[].class);          if (userEmailsResponse == null) {             return "null";         }          var fetchedEmailDetails = Arrays.stream(userEmailsResponse)                 .filter(emailDetails -> emailDetails.verified() && emailDetails.primary())                 .findFirst()                 .orElseGet(() -> null);          return fetchedEmailDetails != null ? fetchedEmailDetails.email() : "null";     } }

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

Далее переходим к традиционному подходу.

Сперва создадим обычные дто для наших будущих ендпоинтов

public record LoginRequest(         String email,         String password ) { }
public record RegistrationRequest(         String email,         String password,         String username ) { }

Следующим нашим шагом станет создание сервисного слоя где будет ядро бизнес логики.

@Service @Slf4j @RequiredArgsConstructor public class AuthService {     private final PasswordEncoder passwordEncoder;     private final UserRepository userRepository;     private final AuthenticationManager authenticationManager;     private final SecurityContextRepository securityContextRepository;      public void register(RegistrationRequest request) {         if (userRepository.existsByEmailAndAuthType(request.email(), AuthType.MANUAL)) {             throw new IllegalArgumentException("Email already registered");         }          var user = new User();         user.setAuthType(AuthType.MANUAL);         user.setUsername(request.username());         user.setEmail(request.email());         user.setPassword(passwordEncoder.encode(request.password()));         userRepository.save(user);     }      public Authentication login(             LoginRequest loginRequest,             HttpServletRequest request,             HttpServletResponse response     ) {         var passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(                 loginRequest.email(), loginRequest.password()         );          var auth = authenticationManager.authenticate(passwordAuthenticationToken);         var securityContext = SecurityContextHolder.createEmptyContext();         securityContext.setAuthentication(auth);         securityContextRepository.saveContext(securityContext, request, response); // сохраняем новую сессию в редис          log.info("Authenticated and created session for {}", auth.getName());         return auth;     }  } 

Отлично, мы создали AuthService с двумя простыми методами. Теперь нам осталось создать контроллер для обработки http запросов.

@RequiredArgsConstructor @RestController @RequestMapping("api/v1/auth") public class AuthController {      private final AuthService authService;      @GetMapping("me")     Principal me(Principal principal) {         return principal;     }      @PostMapping("register")     @ResponseStatus(HttpStatus.CREATED)     void register(@RequestBody RegistrationRequest request) {         authService.register(request);     }      @PostMapping("login")     Object login(             @RequestBody LoginRequest loginRequest,             HttpServletRequest request,             HttpServletResponse response     ) {         return authService                 .login(loginRequest, request, response)                 .getPrincipal();     }  } 

Здесь мы добавили три ендпоинта.

/api/v1/auth/me — Получение текущего пользователя

/api/v1/auth/register — Регистрация

/api/v1/auth/login — Логин

Время тестировать

Как мы видим Spring приложение запущено на порте 8080. После запуска переходим по ссылке http://localhost:8080/oauth2/authorization/github

Затем перед нами откроется страница логина гитхаба. После ввода данных мы попадаем в ендпоинт api/v1/auth/me. В ответ получим данные текущего пользователя.

Перейдем к тестированию традиционного подхода.

Отправляем данный запрос

POST http://localhost:8080/api/v1/auth/register Content-Type: application/json  {   "username": "Daneker" ,   "password": "password" ,   "email": "daneker2005@gmail.com" } 

В ответ получаем статус 201, затем отправляем запрос в api/v1/auth/login

POST http://localhost:8080/api/v1/auth/login Content-Type: application/json  {   "password": "password" ,   "email": "daneker2005@gmail.com" }

После успешного логина получаем в ответ данные текущего пользователя

{   "id": 12,   "username": "Daneker",   "email": "daneker2005@gmail.com",   "authType": "MANUAL",   "role": "ROLE_USER",   "authorities": [     {       "authority": "ROLE_USER"     }   ],   "enabled": true }
Тем временем в редисе созданы две сессии

Тем временем в редисе созданы две сессии

Если у вас появятся вопросы, буду рад ответить на них в комментариях. Также буду благодарен за обратную связь от более опытных и искушённых разработчиков.

Репозиторий проекта здесь.


ссылка на оригинал статьи https://habr.com/ru/articles/815285/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *