Привет! Меня зовут Данекер, я 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 }

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