Как авторизоваться со стороннего клиента с использованием Spring Security?
Сервер на Java (Spring Boot), клиент на React.
Сервер stateless, сессия открывается, но хранится в Redis. Соответственно, всё общение с сервером только по REST. Обычно в таком случае используется базовая аутентификация (передача логина и пароля при каждом запросе), но я пытаюсь сделать аутентификацию на основе cookies.
У меня не получается правильно сконфигурировать Spring Security. Что я хочу:
- Единожды отправляю запрос с логином и паролем на эндпоинт сервера
/login. - Сервер возвращает куку с идентификатором сессии (при этом он возвращает JSON, в котором будет указано, успешно ли прошла аутентификация).
- Клиент спокойно работает, отдавая серверу куку при каждом запросе.
У меня это не получается. Для REST-запросов как правило используют такую конфигурацию:
http
.httpBasic()
Но так как у меня куки, то конфигурация такая:
http
.formLogin()
.loginProcessingUrl("/login")
.usernameParameter("email")
.passwordParameter("password")
Такая конфигурация работает и возвращает куку. Но! При этом Spring отправляет мне ещё и свою стандартную форму запроса, которая мне совсем не нужна, так как форма у меня уже есть на стороне клиента (React).
Как я могу оставить аутентификацию по cookies и при этом дать понять Spring, что мне не нужна его форма?
Ответы (1 шт):
Эм, или я не понял пунктов, или всё довольно просто:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().disable().authorizeRequests()
.anyRequest().authenticated()
.and().formLogin();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
При запросе на любой authenticated() метод происходит редирект на formLogin().
При POST /login выдается кука, которая привязывается к сессии.
В примере используется JdbcUserDetailsManagerConfigurer у которого свой провайдер авторизации, но можно свой провайдер добавить:
public class CustomAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
Для избавления редиректа можно костыльнуть, задизейблив форму .formLogin().disable() и добавив страницу /auth для анонимов: .antMatchers("/auth").anonymous()
И добавив, например, такой контроллер для авторизации:
@GetMapping("auth")
public String auth(@RequestParam String username, @RequestParam String password) throws Exception {
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(passwordEncoder.matches(password, userDetails.getPassword())) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
authentication.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
else {
return "PASSWORD_NOT_MATCHES";
}
}
catch (UsernameNotFoundException ex) {
return "USER_NOT_FOUND";
}
return "OK";
}
Попробуем раскостылить: оставить стандартный механизм авторизации через форму, но без редиректов (уберем редиректы успешной/неуспешной авторизации, редиректы с других страниц):
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().disable().authorizeRequests()
.anyRequest()
.authenticated()
.and().formLogin()
.successHandler((httpServletRequest, httpServletResponse, authentication) -> httpServletResponse.setStatus(HttpStatus.OK.value()))
.failureHandler((httpServletRequest, httpServletResponse, e) -> httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()))
.and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login") {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if(authException != null) {
response.sendError(HttpStatus.FORBIDDEN.value());
}
}
});
}