Spring Security
Java EE security
- Lacks depth for typical enterprise applications
- Not portable between environments (WAR/EAR)
Spring security
- Authentication - establishing principal (user, device, system)
- Authorizatino - can principal execute operation?
Authentication
- HTTP BASIC authentication headers (an IETF RFC-based standard)
- HTTP Digest authentication headers (an IETF RFC-based standard)
- HTTP X.509 client certificate exchange (an IETF RFC-based standard)
- LDAP (a very common approach to cross-platform authentication needs, especially in large environments)
- Form-based authentication (for simple user interface needs)
- OpenID authentication
- ...
- ...
- ...
Authorisation
- Web requests
- Method invocation
- Access to individual domain object instance
Packages
- spring-security-core.jar
- spring-security-remoting.jar
- spring-security-web.jar
- spring-security-config.jar
- spring-security-ldap.jar
- spring-security-acl.jar
- spring-security-cas.jar
- spring-security-test.jar
Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}<http>
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
</http>Form login
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll(); Authorisation
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
// ...
.formLogin();
}Handling logouts
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(logoutSuccessHandler)
.invalidateHttpSession(true)
.addLogoutHandler(logoutHandler)
.deleteCookies(cookieNamesToClear)
.and()
...
}LogoutHandler and LogoutSuccessHandler
- PersistentTokenBasedRememberMeServices
- TokenBasedRememberMeServices
- CookieClearingLogoutHandler
- CsrfLogoutHandler
- SecurityContextLogoutHandler
- SimpleUrlLogoutSuccessHandler
- HttpStatusReturningLogoutSuccessHandler
Authentication
- In memory
- JDBC
- LDAP
- Custom
In memory authentication
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
}JDBC authentication
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
}LDAP authentication
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups");
}dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password
dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password
dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=orgCustom AuthenticationProvider
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
return new SpringAuthenticationProvider();
}Custom UserDetailsService and PasswordEncoder
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
return new SpringDataUserDetailsService();
}@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}Method security (@Secured)
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}Method security (@PreAuthorize)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}Method security and GlobalMethodSecurityConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}XML Configuration
web.xml
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>xmlns:security
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
...
</beans><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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
...
</beans:beans>Minimal <http> configuration
<http>
<intercept-url pattern="/**" access="hasRole('USER')" />
<form-login />
<logout />
</http>Authentication
<authentication-manager>
<authentication-provider>
<user-service>
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>Authentication
<authentication-manager>
<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager><authentication-manager>
<authentication-provider>
<jdbc-user-service data-source-ref="securityDataSource"/>
</authentication-provider>
</authentication-manager><authentication-manager>
<authentication-provider ref='myAuthenticationProvider'/>
</authentication-manager>Password encoder
<beans:bean name="bcryptEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<authentication-manager>
<authentication-provider>
<password-encoder ref="bcryptEncoder"/>
<user-service>
<user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f"
authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f"
authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>Form login
<http>
<intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page='/login.jsp'/>
</http><http pattern="/css/**" security="none"/>
<http pattern="/login.jsp*" security="none"/>
<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page='/login.jsp'/>
</http><http pattern="/login.htm*" security="none"/>
<http use-expressions="false">
<intercept-url pattern='/**' access='ROLE_USER' />
<form-login login-page='/login.htm' default-target-url='/home.htm'
always-use-default-target='true' />
</http>Basic authentication
<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<http-basic />
</http>Filters
- http://docs.spring.io/spring-security/site/docs/4.1.2.BUILD-SNAPSHOT/reference/html/ns-config.html#ns-custom-filters
- We can create our own filters
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>
<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter"/><global-method-security>
<global-method-security secured-annotations="enabled" />public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}<global-method-security pre-post-annotations="enabled" />public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}Security pointcuts
<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
access="ROLE_USER"/>
</global-method-security>Authentication Internals
SecurityContextHolder
- Stores SecurityContext
- Default store method is ThreadLocal but can be changed
- SecurityContextHolder.getContext()
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}UserDetails
- Adapter between Spring Security and databases
- Provided by UserDetailsService
- Multiple builtin implementations: InMemoryDaoImpl, JdbcDaoImpl
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}GrantedAuthority
- Usually roles (ROLE_ADMINISTRATOR, ROLE_HR_SUPERVISOR)
Collection<? extends GrantedAuthority> authorities =
SecurityContextHolder.getContext().getAuthentication().getAuthorities();public interface GrantedAuthority extends Serializable {
String getAuthority();
}Authentication
- Container Managed Authentication is not recommended
- Building UsernamePasswordAuthenticationToken (not fully populated of Authentication)
- Token is passed to AuthenticationManager
- On successful validation, AuthenticationManager returns fully pupulated Authentication
- SecurityContext is established and Authentication set
Authentication
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}AuthenticationManager
- Default implemenation is ProviderManager
- Delegates to AuthenticationProviders
- AuthenticationProvider returns Authentication, null if not supported or throws an Exception
DaoAuthenticationProvider
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}UserDetailsService
- InMemoryUserDetailsManager
- JdbcDaoImpl
- Uses JDBC instead of ORM
PasswordEncoder
- StandardPasswordEncoder (not recommended)
- NoOpPasswordEncoder (for testing)
- BCryptPasswordEncoder (best option!)
Authorization (Access Control) Internals
Internals

AccessDecisionManager
public interface AccessDecisionManager {
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}Secure Object
- Object with security appled on it (method invocation, http requests, ...)
- AbstractSecurityInterceptor for every secure object
- MethodSecurityInterceptor
- FilterSecurityInterceptor
- AspectJMethodSecurityInterceptor
AbstractSecurityInterceptor
- Looks up the configuration attributes for request
- Invokes AccessDecisionManager with Authentication object
- Changes Authentication if necessary
- Proceeds secured object invocation on ganted permission
- Calls the AfterInvocationManager (if configured)
Testing
Given the service...
@service
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return "Hello " + authentication;
}
}Not authenticated
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}@WithMockUser
@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
String message = messageService.getMessage();
}@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
}@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
...
}@WithAnonymousUser
@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {
@Test
public void withMockUser1() {
}
@Test
public void withMockUser2() {
}
@Test
@WithAnonymousUser
public void anonymous() throws Exception {
// override default to run as anonymous user
}
}@WithUserDetails
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
String message = messageService.getMessage();
...
}Meta annotations
@WithMockUser(username="admin",roles={"USER","ADMIN"})@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }Spring Security
By Artur Owczarek
Spring Security
- 610