Я разрабатываю веб-приложение с использованием архитектуры Spring MVC и защищаю его с помощью безопасности Spring. Я использую репозитории JPA для своего уровня сохраняемости. Проблема, с которой я столкнулся, заключается в том, что когда я пытаюсь отправить запрос POST с определенной страницы в приложении (страница «Добавить реализацию»), я получаю страницу с ошибкой с этим сообщением:
There was an unexpected error (type=Forbidden, status=403). Forbidden
Это происходит независимо от роли моего пользователя (есть две роли: admin и vendor). Кроме того, это происходит даже тогда, когда я явно разрешаю соответствующий URL-адрес в моей функции configure(HttpSecurity http), используя antMatchers и permitAll(). Возникает вопрос, почему мой запрос POST не авторизуется?
Я новичок в Spring Security и, возможно, допустил критическую ошибку в любой из конфигураций в нем. Я прикреплю весь код, связанный с безопасностью Spring, а также рассматриваемый контроллер.
Ниже представлена моя функция настройки: URL-адрес /vendor/{id:[0-9]+}/addimpl вызывает у меня проблемы. Я явно разрешил это здесь, чтобы посмотреть, что произойдет, но я все еще получаю сообщение об ошибке 403 при отправке на него (но запрос GET работает нормально).
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private AcvpUserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register", "/webjars/**", "/css/**",
"/images/**").permitAll()
.antMatchers("/vendor/{id:[0-9]+}/addimpl").permitAll()
.anyRequest().authenticated().and().formLogin()
.loginPage("/login").loginProcessingUrl("/login").successHandler(myAuthenticationSuccessHandler()).permitAll().and()
.logout().permitAll();
}
Вот мой UserDetailsService:
@Service
@Transactional
public class AcvpUserDetailsService implements UserDetailsService {
@Autowired
private AcvpUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
AcvpUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new AcvpUserPrincipal(user);
}
}
И класс UserDetails ...
@Transactional
public class AcvpUserPrincipal implements UserDetails {
/**
* this is necessary for posterity to know whether they can serialize this
* class safely
*/
private static final long serialVersionUID = 3771770649711489402L;
private AcvpUser user;
public AcvpUserPrincipal(AcvpUser user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new
SimpleGrantedAuthority(user.getRole()));
}
@Override
public String getPassword() {
return user.getPassword(); // this is now the encrypted password
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
А вот и мой класс AuthenticationSuccessHandler. Пример этого возвращается из myAuthenticationSuccessHandler() в функции настройки.
public class AcvpAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
protected Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private AcvpUserRepository userRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
protected void handle(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException {
String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug(
"Response has already been committed. Unable to redirect to "
+ targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(Authentication authentication) {
boolean isUser = false;
boolean isAdmin = false;
Collection<? extends GrantedAuthority> authorities
= authentication.getAuthorities();
for (GrantedAuthority grantedAuthority : authorities) {
if (grantedAuthority.getAuthority().equals(AcvpRoles.VENDOR_ROLE)) {
isUser = true;
break;
} else if (grantedAuthority.getAuthority().equals(AcvpRoles.ADMIN_ROLE)) {
isAdmin = true;
break;
}
}
if (isUser) {
String username = authentication.getName();
AcvpUser user = userRepository.findByUsername(username);
return "/vendor/" + user.getVendor().getId();
} else if (isAdmin) {
return "/";
} else {
throw new IllegalStateException();
}
}
protected void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return redirectStrategy;
}
}
Вот зависимости Spring Security из моего файла pom:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>runtime</scope>
</dependency>
Вот функция контроллера. Я включаю как GET, так и POST, но обратите внимание, что GET работает нормально, а POST выдает ошибку. Я отмечу, однако, что программа НЕ входит в функцию POST. Я установил точку останова и попытался выполнить отладку, но перед входом в эту функцию произошел сбой.
@RequestMapping(value = "/vendor/{id:[0-9]+}/addimpl", method = RequestMethod.GET)
public String getAddImplementation(Model model, @PathVariable("id") Long id)
throws VendorNotFoundException {
Vendor vendor = vendorRepository.findById(id)
.orElseThrow(VendorNotFoundException::new);
model.addAttribute("vendor", vendor);
model.addAttribute("edit", false);
model.addAttribute("moduleTypes", ModuleType.values());
ImplementationAddForm backingObject = new ImplementationAddForm();
model.addAttribute("form", backingObject);
return "implementation-add-edit";
}
@RequestMapping(value = "/vendor/{id:[0-9]+}/addimpl", method = RequestMethod.POST)
public String saveImplementation(@PathVariable("id") Long id,
@ModelAttribute("implementation") @Valid ImplementationAddForm form,
BindingResult bindingResult, Model model, RedirectAttributes ra)
throws VendorNotFoundException {
Vendor vendor = vendorRepository.findById(id)
.orElseThrow(VendorNotFoundException::new);
if (bindingResult.hasErrors()) {
model.addAttribute("vendor", vendor);
model.addAttribute("edit", false);
model.addAttribute("moduleTypes", ModuleType.values());
model.addAttribute("form", form);
return "implementation-add-edit";
} else {
Implementation i = form.buildEntity();
i.setVendor(vendor);
implementationRepository.save(i);
return "redirect:/vendor/" + id;
}
}
Наконец, я включаю результат установки «отладки» в классе SecurityConfiguration. Это могло помочь, но я ничего от этого не получил.
Request received for POST '/vendor/33/addimpl':
org.apache.catalina.connector.RequestFacade@3b981cfd
servletPath:/vendor/33/addimpl
pathInfo:null
headers:
host: localhost:8080
connection: keep-alive
content-length: 402
cache-control: max-age=0
origin: http://localhost:8080
upgrade-insecure-requests: 1
content-type: application/x-www-form-urlencoded
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
referer: http://localhost:8080/vendor/33/addimpl
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cookie: JSESSIONID=1121ADD15A2E23786464649647B62356
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
************************************************************
2018-12-21 09:49:32.570 INFO 4392 --- [nio-8080-exec-3] Spring Security
Debugger :
************************************************************
Request received for POST '/error':
org.apache.catalina.core.ApplicationHttpRequest@73210c23
servletPath:/error
pathInfo:null
headers:
host: localhost:8080
connection: keep-alive
content-length: 402
cache-control: max-age=0
origin: http://localhost:8080
upgrade-insecure-requests: 1
content-type: application/x-www-form-urlencoded
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
referer: http://localhost:8080/vendor/33/addimpl
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cookie: JSESSIONID=1121ADD15A2E23786464649647B62356
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
После отправки запроса POST на /vendor/33/addimpl я ожидаю, что в случае ошибки проверки меня снова перенаправят обратно на страницу поставщика или на страницу «добавить реализацию» (ту самую страницу, с которой я отправил). Но ничего из этого не происходит. Вместо этого меня отправляют на страницу с ошибкой по умолчанию.




CSRF (подделка межсайтовых запросов) включен по умолчанию.
Вы можете отключить его в своем классе AcvpUserDetailsService.
Добавлять:
http.csrf().disable();
Подробнее о CSRF здесь: https://www.baeldung.com/spring-security-csrf
Спасибо! Это сработало. Но стоит ли просто отключить csrf? Почему мое приложение не предоставляет токен csrf с запросом?
Похоже, я могу найти ответы в ссылках, по которым вы и Анджело Иммедиата меня порекомендовали. Спасибо.
Как говорили другие, проблема заключалась в том, что CSRF был включен, а токен CSRF не отправлялся с запросом POST. Однако я не хотел полностью отключать CSRF, так как хотел, чтобы приложение было защищено от атак CSRF. Оказывается, добавить токен CSRF в это приложение очень просто. Я использую тимелеаф в качестве инструмента для создания шаблонов, и этого простого решения нет ни в одной из уже опубликованных ссылок, но его можно найти здесь: https://www.baeldung.com/csrf-thymeleaf-with-spring-security
Я включил этот код в свою форму входа:
<input type = "hidden" th:name = "${_csrf.parameterName}" th:value = "${_csrf.token}" />
Согласно приведенной выше ссылке, это все, что необходимо, но для меня это не сработало, пока я не добавил нотацию тимелеафа th: ко всем своим действиям с формой. Поэтому вместо <form action = "<url>" мне пришлось сделать <form th:action = "@{<url>}".
Я не вижу токен CSRF в запросе POST. По умолчанию в Spring Security он включен. docs.spring.io/spring-security/site/docs/current/reference/…