Статьи

Безопасные службы REST с использованием Spring Security

обзор

Недавно я работал над проектом, который использует уровень сервисов REST для связи с клиентским приложением (приложение GWT). Поэтому я потратил много времени, чтобы выяснить, как защитить REST-сервисы с помощью Spring Security. В этой статье описывается решение, которое я нашел, и я реализовал. Я надеюсь, что это решение будет кому-то полезно и сэкономит много драгоценного времени.

Решение

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

В нашем сценарии все по-другому, потому что у нас нет страниц для пересылки, нам нужно адаптировать и переопределить Spring Security для связи только с использованием статуса HTTP-протоколов, ниже я перечисляю, что нужно сделать, чтобы Spring Security работала лучше всего:

  • Аутентификация будет осуществляться с помощью обычной формы входа, единственное отличие состоит в том, что ответ будет в JSON вместе с HTTP-статусом, который может быть либо кодом 200 (если аутентификация пройдена), либо кодом 401 (если аутентификация не удалась);
  • Переопределите AuthenticationFailureHandler, чтобы вернуть код 401 UNAUTHORIZED;
  • Переопределите AuthenticationSuccessHandler, чтобы вернуть код 20 OK, тело ответа HTTP содержит данные JSON текущего аутентифицированного пользователя;
  • Переопределите AuthenticationEntryPoint, чтобы всегда возвращать код 401 UNAUTHORIZED. Это заменит поведение по умолчанию Spring Security, которое перенаправляет пользователя на страницу входа в систему, если он не отвечает требованиям безопасности, потому что в REST у нас нет страницы входа;
  • Переопределите LogoutSuccessHandler, чтобы вернуть код 20 OK;

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

Примечание. Для следующего решения требуется Spring Security в версии не ниже 3.2.

Переопределение AuthenticationEntryPoint

Класс расширяет org.springframework.security.web.AuthenticationEntryPoint и реализует только один метод, который отправляет ошибку ответа (с кодом состояния 401) в случае неавторизованной попытки.

1
2
3
4
5
6
7
8
@Component
public class HttpAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

Переопределение AuthenticationSuccessHandler

AuthenticationSuccessHandler отвечает за то, что делать после успешной аутентификации, по умолчанию он перенаправляет на URL, но в нашем случае мы хотим, чтобы он отправлял HTTP-ответ с данными.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthSuccessHandler.class);
 
    private final ObjectMapper mapper;
 
    @Autowired
    AuthSuccessHandler(MappingJackson2HttpMessageConverter messageConverter) {
        this.mapper = messageConverter.getObjectMapper();
    }
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_OK);
 
        NuvolaUserDetails userDetails = (NuvolaUserDetails) authentication.getPrincipal();
        User user = userDetails.getUser();
        userDetails.setUser(user);
 
        LOGGER.info(userDetails.getUsername() + " got is connected ");
 
        PrintWriter writer = response.getWriter();
        mapper.writeValue(writer, user);
        writer.flush();
    }
}

Переопределение AuthenticationFailureHandler

AuthenticationFaillureHandler отвечает за то, что делать после неудачной аутентификации, по умолчанию он перенаправляет на URL страницы входа, но в нашем случае мы просто хотим, чтобы он отправил HTTP-ответ с кодом 401 UNAUTHORIZED.

01
02
03
04
05
06
07
08
09
10
11
12
@Component
public class AuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
 
        PrintWriter writer = response.getWriter();
        writer.write(exception.getMessage());
        writer.flush();
    }
}

Переопределение LogoutSuccessHandler

LogoutSuccessHandler решает, что делать, если пользователь успешно вышел из системы, по умолчанию он будет перенаправлять на URL страницы входа в систему, потому что у нас его нет, я переопределил его, чтобы вернуть HTTP-ответ с кодом 20 OK.

1
2
3
4
5
6
7
8
9
@Component
public class HttpLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().flush();
    }
}

Конфигурация безопасности Spring

Это последний шаг, чтобы сложить все то, что мы сделали, я предпочитаю использовать новый способ настройки Spring Security с Java без XML, но вы можете легко адаптировать эту конфигурацию к XML.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String LOGIN_PATH = ApiPaths.ROOT + ApiPaths.User.ROOT + ApiPaths.User.LOGIN;
 
    @Autowired
    private NuvolaUserDetailsService userDetailsService;
    @Autowired
    private HttpAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AuthSuccessHandler authSuccessHandler;
    @Autowired
    private AuthFailureHandler authFailureHandler;
    @Autowired
    private HttpLogoutSuccessHandler logoutSuccessHandler;
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
 
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(new ShaPasswordEncoder());
 
        return authenticationProvider;
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }
 
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authenticationProvider(authenticationProvider())
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .formLogin()
                .permitAll()
                .loginProcessingUrl(LOGIN_PATH)
                .usernameParameter(USERNAME)
                .passwordParameter(PASSWORD)
                .successHandler(authSuccessHandler)
                .failureHandler(authFailureHandler)
                .and()
                .logout()
                .permitAll()
                .logoutRequestMatcher(new AntPathRequestMatcher(LOGIN_PATH, "DELETE"))
                .logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .sessionManagement()
                .maximumSessions(1);
 
        http.authorizeRequests().anyRequest().authenticated();
    }
}

Это был пробный пик в общей конфигурации, я прикрепил в этой статье репозиторий Github, содержащий образец проекта https://github.com/imrabti/gwtp-spring-security .

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

Ссылка: Защищайте REST-сервисы с помощью Spring Security от нашего партнера по JCG Идрисса Мрабти в блоге Fancy UI .