I'm trying to get through Spring Security. I have to implement a custom login form, so I need to understand very well what my configurations mean.
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");
}
}
I know I can map the /login URL with RequestMethod.GET, but when I try to intercept the POST after form submit it doesn't work.
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())));
}
}
N.B. User's data are not in my db at first, that's because I'm not sure about the UserDetailsService solution (in which UserDetails are loaded simply by username). To retrieve my Customer object I need both username and password (to send to a specific external URL) then, if the JSON response is positive (username and password are correct), I have to send 2 others HTTP request to get Customer's data as firstname, lastname, nationality, etc. At this point my user can be considered logged in.
Any suggestions? Thanks in advance.
- I believe, but need to confirm, that is because Security is doing something behind the scenes: gets username and password values from the posted form and compare them with the ones in the authentication provider: if these match, default-target-url is shown, else user must repeat the login. Is it right?
That's right. When you declare a <login-form>
element in the security config you are configuring a UsernamePasswordAuthenticationFilter.
There you configure some url's:
@RequestMapping
which returns the login formspring-security
the equivalent to build a post processing controller method.While login-processing-url, default-target-url and authentication-failure-url must be valid RequestMappings, the login-processing-url won't reach the Spring MVC controller layer as it is executed before hitting the Spring MVC dispatcher servlet.
So the
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView doLogin() {
System.out.println("***LOGIN_POST***");
return new ModelAndView("users/home");
}
won't be never reached.
When a POST is made to /login
uri, the UsernamePasswordAuthenticationFilter will perform it's doFilter()
method to catch the user provided credentials, build a UsernamePasswordAuthenticationToken and delegate it to the AuthenticationManager, where this Authentication will be executed in the matching AuthenticationProvider.
- Then my problem is: I need username and password values typed in the Security's login form, because I have to send an HTTP request to an external server to verify if these match. Before to introduce Security I developed this mechanism using /login GET and /login POST, with @ModelAttribute annotation. How can I do now? I suppose when you used to perform the authentication to the external server you did it by delegating to a class from the POST /login RequestMapping.
So simply create a custom AuthenticationProvider which delegates the user validation stuff to your old logic:
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;
}
}
And the security config 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>
- Changing the authentication-provider, using a class which implements UserDetailsService, what happens? I believe that, in this case, username and password typed in the login form are compared with the ones retrieved from the db, as these are assigned to the User object. Is it right?
As you said that you must send both username and password, I don't think that UserServiceDetails schema fits your requirements. I thin you should do it as I suggested in point 2.
EDIT:
One last thing: now I'm sending HTTP request in authenticate method, if credentials are correct I receive a token in the response, which I need to get access to other external server services. How can I pass it in my Spring controller?
To receive and handle the received token, I would do it like this:
The ExternalAuthenticationValidator interface:
public interface ExternalAuthenticationValidator {
public abstract ThirdPartyValidationResponse validate(String name, String password);
}
The ThirdPartyValidationResponse model interface:
public interface ThirdPartyValidationResponse{
public boolean isValid();
public Serializable getToken();
}
Then, change the way the Provider handles and manages it:
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;
}
}
Now, you must use this code snippet to retrieve the token from the userDetails:
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 && details instanceof ThirdPartyValidationResponse){
return ((ThirdPartyValidationResponse)details).getToken();
}
}
return null;
Instead of including it everywhere you need it, it may be better to create a class which retrieves it from the details of the authentication:
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 && details instanceof ThirdPartyValidationResponse){
return ((ThirdPartyValidationResponse)details).getToken();
}
}
return null;
}
}
In case you chose this last way, just declare it in the security xml config (or annotate with a @Service
, etc annotation):
<beans:bean id="tokenRetriever" class="com.xxx.yyy.SecurityContextThirdPartyTokenRetriever" />
There are other approaches, such as extending UsernamePasswordAuthenticationToken to include the token as a field on it, but this is the easiest one I think.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With