#java #authentication #login #spring-security #logout
#java #аутентификация #войти #spring-безопасность #Выход
Вопрос:
Я пытаюсь пройти через Spring Security. Я должен реализовать пользовательскую форму входа, поэтому мне нужно очень хорошо понимать, что означают мои конфигурации.
spring-security.xml
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.0.xsd">
<http auto-config="true">
<intercept-url pattern="/user**" access="isAuthenticated()" />
<form-login authentication-failure-url="/login" login-page="/login"
login-processing-url="/login" default-target-url="/user" />
<logout invalidate-session="true" logout-success-url="/index"
logout-url="/logout" />
</http>
<authentication-manager id="custom-auth">
<authentication-provider>
<user-service>
<user name="my_username" password="my_password"
authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
LoginController
@Controller
public class LoginController {
[....]
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView doLogin() {
System.out.println("***LOGIN_POST***");
return new ModelAndView("users/home");
}
@RequestMapping(value = "/logout", method = RequestMethod.POST)
public ModelAndView doLogout() {
System.out.println("***LOGOUT_POST***");
return new ModelAndView("index");
}
}
Я знаю, что могу сопоставить URL-адрес /login с RequestMethod .ПОЛУЧИТЬ, но когда я пытаюсь перехватить сообщение после отправки формы, оно не работает.
- Я считаю, но должен подтвердить, что это связано с тем, что Security делает что-то за кулисами: получает значения имени пользователя и пароля из опубликованной формы и сравнивает их с значениями в поставщике аутентификации: если они совпадают, отображается default-target-url, иначе пользователь должен повторить вход в систему. Правильно ли это?
- Тогда моя проблема: мне нужны значения имени пользователя и пароля, введенные в форме входа в систему безопасности, потому что я должен отправить HTTP-запрос на внешний сервер, чтобы проверить, совпадают ли они. Прежде чем вводить безопасность, я разработал этот механизм, используя /login GET и /login POST с аннотацией @ModelAttribute . Что я могу сделать сейчас?
- Изменение поставщика аутентификации с использованием класса, который реализует UserDetailsService, что происходит? Я полагаю, что в этом случае имя пользователя и пароль, введенные в форме входа, сравниваются с теми, которые были получены из базы данных, поскольку они назначены объекту User. Правильно ли это?
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private CustomerDao customerDao;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Customer customer = customerDao.findCustomerByUsername(username);
return new User(customer.getUsername(), customer.getPassword(), true, true, true, true,
Arrays.asList(new SimpleGrantedAuthority(customer.getRole())));
}
}
Сначала данные пользователя не находятся в моей базе данных, это потому, что я не уверен в решении UserDetailsService (в котором пользовательские данные загружаются просто по имени пользователя). Для получения моего объекта Customer мне нужны как имя пользователя, так и пароль (для отправки на определенный внешний URL-адрес), затем, если ответ JSON положительный (имя пользователя и пароль верны), я должен отправить 2 других HTTP-запроса, чтобы получить данные клиента в виде имени, фамилии, национальности и т.д. На данный момент мой пользователь может считаться вошедшим в систему.
Есть предложения? Заранее спасибо.
Ответ №1:
- Я считаю, но должен подтвердить, что это связано с тем, что Security делает что-то за кулисами: получает значения имени пользователя и пароля из опубликованной формы и сравнивает их с значениями в поставщике аутентификации: если они совпадают, отображается default-target-url, иначе пользователь должен повторить вход в систему. Правильно ли это?
Это верно. Когда вы объявляете <login-form>
элемент в конфигурации безопасности, вы настраиваете UsernamePasswordAuthenticationFilter .
Там вы настраиваете некоторые URL-адреса:
- login-page=»/login»: URL-адрес, который указывает на a
@RequestMapping
, который возвращает форму входа - login-processing-url=»/login»: URL, который запускает UsernamePasswordAuthenticationFilter . Это
spring-security
эквивалентно созданию метода контроллера постобработки. - default-target-url=»/user»: страница по умолчанию, на которую пользователь будет перенаправлен после предоставления действительных учетных данных пользователя.
- ошибка аутентификации-url=»/ login»: URL, на который пользователь будет перенаправлен в случае попытки входа с неверными учетными данными.
В то время как URL-адреса для обработки входа, URL-адреса по умолчанию и URL-адреса для проверки подлинности должны быть допустимыми приложениями для запросов, URL-адрес для обработки входа не достигнет уровня контроллера Spring MVC, поскольку он выполняется перед запуском сервлета диспетчера Spring MVC.
Итак,
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView doLogin() {
System.out.println("***LOGIN_POST***");
return new ModelAndView("users/home");
}
никогда не будет достигнут.
Когда POST отправляется в /login
uri, UsernamePasswordAuthenticationFilter выполнит doFilter()
свой метод, чтобы перехватить предоставленные пользователем учетные данные, создать UsernamePasswordAuthenticationToken и делегировать его AuthenticationManager, где эта аутентификация будет выполнена в соответствующем AuthenticationProvider.
- Тогда моя проблема: мне нужны значения имени пользователя и пароля, введенные в форме входа в систему безопасности, потому что я должен отправить HTTP-запрос на внешний сервер, чтобы проверить, совпадают ли они. Прежде чем вводить безопасность, я разработал этот механизм, используя /login GET и /login POST с аннотацией @ModelAttribute . Что я могу сделать сейчас? Я полагаю, что когда вы выполняли аутентификацию на внешнем сервере, вы делали это, делегируя классу из POST / login RequestMapping .
Итак, просто создайте пользовательский AuthenticationProvider, который делегирует материал проверки пользователя вашей старой логике:
public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {
private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;
// This represents your existing username/password validation class
// Bind it with an @Autowired or set it in your security config
private ExternalAuthenticationValidator externalAuthenticationValidator;
/* (non-Javadoc)
* @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
boolean validated = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
if(!validated){
throw new BadCredentialsException("username and/or password not valid");
}
Collection<? extends GrantedAuthority> authorities = null;
// you must fill this authorities collection
return new UsernamePasswordAuthenticationToken(
authentication.getName(),
authentication.getCredentials(),
authorities
);
}
/* (non-Javadoc)
* @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> authentication) {
return this.supportingClass.isAssignableFrom(authentication);
}
public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
return externalAuthenticationValidator;
}
public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
this.externalAuthenticationValidator = externalAuthenticationValidator;
}
}
И xml конфигурации безопасности:
<beans:bean id="thirdPartyAuthenticationProvider" class="com.xxx.yyy.ThirdPartyAuthenticationProvider">
<!-- here set your external authentication validator in case you can't autowire it -->
<beans:property name="externalAuthenticationValidator" ref="yourExternalAuthenticationValidator" />
</beans:bean>
<security:authentication-manager id="custom-auth">
<security:authentication-provider ref="thirdPartyAuthenticationProvider" />
</security:authentication-manager>
<security:http auto-config="true" authentication-manager-ref="custom-auth">
<security:intercept-url pattern="/user**" access="isAuthenticated()" />
<security:form-login authentication-failure-url="/login" login-page="/login"
login-processing-url="/login" default-target-url="/user" />
<security:logout invalidate-session="true" logout-success-url="/index"
logout-url="/logout" />
<!-- in spring security 4.x CSRF filter is enabled by default. Disable it if
you don't plan to use it, or at least in the first attempts -->
<security:csrf disabled="true"/>
</security:http>
- Изменение поставщика аутентификации с использованием класса, который реализует UserDetailsService, что происходит? Я полагаю, что в этом случае имя пользователя и пароль, введенные в форме входа, сравниваются с теми, которые были получены из базы данных, поскольку они назначены объекту User. Правильно ли это?
Поскольку вы сказали, что вы должны отправить как имя пользователя, так и пароль, я не думаю, что схема UserServiceDetails соответствует вашим требованиям. Я думаю, вам следует сделать это так, как я предложил в пункте 2.
Редактировать:
И последнее: теперь я отправляю HTTP-запрос в методе аутентификации, если учетные данные верны, я получаю токен в ответе, который мне нужен для получения доступа к другим внешним серверным службам. Как я могу передать его в свой Spring controller?
Чтобы получить и обработать полученный токен, я бы сделал это следующим образом:
Интерфейс ExternalAuthenticationValidator:
public interface ExternalAuthenticationValidator {
public abstract ThirdPartyValidationResponse validate(String name, String password);
}
Интерфейс модели третьей части validationresponse:
public interface ThirdPartyValidationResponse{
public boolean isValid();
public Serializable getToken();
}
Затем измените способ, которым Поставщик обрабатывает его и управляет им:
public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {
private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;
private ExternalAuthenticationValidator externalAuthenticationValidator;
/* (non-Javadoc)
* @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ThirdPartyValidationResponse response = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
if(!response.isValid()){
throw new BadCredentialsException("username and/or password not valid");
}
Collection<? extends GrantedAuthority> authorities = null;
// you must fill this authorities collection
UsernamePasswordAuthenticationToken authenticated =
new UsernamePasswordAuthenticationToken(
authentication.getName(),
authentication.getCredentials(),
authorities
);
authenticated.setDetails(response);
return authenticated;
}
/* (non-Javadoc)
* @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> authentication) {
return this.supportingClass.isAssignableFrom(authentication);
}
public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
return externalAuthenticationValidator;
}
public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
this.externalAuthenticationValidator = externalAuthenticationValidator;
}
}
Теперь вы должны использовать этот фрагмент кода для извлечения токена из пользовательских данных:
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
if(auth == null){
throw new IllegalAccessException("Authentication is null in SecurityContext");
}
if(auth instanceof UsernamePasswordAuthenticationToken){
Object details = auth.getDetails();
if(details != null amp;amp; details instanceof ThirdPartyValidationResponse){
return ((ThirdPartyValidationResponse)details).getToken();
}
}
return null;
Вместо того, чтобы включать его везде, где вам это нужно, может быть лучше создать класс, который извлекает его из деталей аутентификации:
public class SecurityContextThirdPartyTokenRetriever {
public Serializable getThirdPartyToken() throws IllegalAccessException{
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
if(auth == null){
throw new IllegalAccessException("Authentication is null in SecurityContext");
}
if(auth instanceof UsernamePasswordAuthenticationToken){
Object details = auth.getDetails();
if(details != null amp;amp; details instanceof ThirdPartyValidationResponse){
return ((ThirdPartyValidationResponse)details).getToken();
}
}
return null;
}
}
В случае, если вы выбрали этот последний способ, просто объявите его в конфигурации xml безопасности (или аннотируйте @Service
аннотацией etc).:
<beans:bean id="tokenRetriever" class="com.xxx.yyy.SecurityContextThirdPartyTokenRetriever" />
Существуют и другие подходы, такие как расширение UsernamePasswordAuthenticationToken для включения в него токена в качестве поля, но я думаю, что это самый простой.
Комментарии:
1. Спасибо за разъяснения! И последнее: теперь я отправляю HTTP-запрос в методе аутентификации, если учетные данные верны, я получаю токен в ответе, который мне нужен для получения доступа к другим внешним серверным службам. Как я могу передать его в свой Spring controller?
2. Сработало! Большое спасибо