I am currently building a single page AngularJS application which communicates via REST to a backend. The structure is as follow:
One Spring MVC WebApp project which contains all AngularJS pages and resources and all REST controllers.
A true backend which has services and repositories for backend communication, an API if you will. The REST calls will talk to these service (the second project is included as a dependency of the first one).
I have been thinking about this a lot but I can't seem to find anything that can help me. Basically I just need some security on this application. I'd like some kind of session management which is extremely simple:
This is the general idea of basic session managament, what would be the easiest way to get this implemented in a Spring MVC webapp (no JSP's, just angular and REST controllers).
Thanks in advance!
By default, Spring Security will create a session when it needs one — this is “ifRequired“. For a more stateless application, the “never” option will ensure that Spring Security itself won't create any session. But if the application creates one, Spring Security will make use of it.
Conclusion. We've learned how to implement a Spring Security login page with Angular. From version 4 onwards, we can make use of the Angular CLI project for easy development and testing. As always all the example we've discussed here can be found over GitHub project.
You have 2 options for the rest API: stateful or stateless.
1st option: HTTP session authentication - the "classical" Spring Security authentication mechanism. If you plan to scale your application on multiple servers, you need to have a load balancer with sticky sessions so that each user stays on the same server (or use Spring Session with Redis).
2nd option: you have the choice of OAuth or token-based authentication.
OAuth2 is a stateless security mechanism, so you might prefer it if you want to scale your application across several machines. Spring Security provides an OAuth2 implementation. The biggest issue with OAuth2 is that requires to have several database tables in order to store its security tokens.
Token-based authentication, like OAuth2, is a stateless security mechanism, so it's another good option if you want to scale on several different servers. This authentication mechanism doesn't exist by default with Spring Security. It is easier to use and implement than OAuth2, as it does not require a persistence mechanism, so it works on all SQL and NoSQL options. This solution uses a custom token, which is a MD5 hash of your user name, the expiration date of the token, your password, and a secret key. This ensures that if someone steals your token, he should not be able to extract your username and password.
I recommend you to look into JHipster. It will generate a web app skeleton for you with REST API using Spring Boot and the front end using AngularJS. When generating the application skeleton it will ask you to choose between the 3 authentication mechanisms that I described above. You can reuse the code that JHipster will generate in your Spring MVC application.
Here is an example of TokenProvider generated by JHipster:
public class TokenProvider {
private final String secretKey;
private final int tokenValidity;
public TokenProvider(String secretKey, int tokenValidity) {
this.secretKey = secretKey;
this.tokenValidity = tokenValidity;
}
public Token createToken(UserDetails userDetails) {
long expires = System.currentTimeMillis() + 1000L * tokenValidity;
String token = userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires);
return new Token(token, expires);
}
public String computeSignature(UserDetails userDetails, long expires) {
StringBuilder signatureBuilder = new StringBuilder();
signatureBuilder.append(userDetails.getUsername()).append(":");
signatureBuilder.append(expires).append(":");
signatureBuilder.append(userDetails.getPassword()).append(":");
signatureBuilder.append(secretKey);
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No MD5 algorithm available!");
}
return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes())));
}
public String getUserNameFromToken(String authToken) {
if (null == authToken) {
return null;
}
String[] parts = authToken.split(":");
return parts[0];
}
public boolean validateToken(String authToken, UserDetails userDetails) {
String[] parts = authToken.split(":");
long expires = Long.parseLong(parts[1]);
String signature = parts[2];
String signatureToMatch = computeSignature(userDetails, expires);
return expires >= System.currentTimeMillis() && signature.equals(signatureToMatch);
}
}
SecurityConfiguration:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
@Inject
private UserDetailsService userDetailsService;
@Inject
private TokenProvider tokenProvider;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/protected/**").authenticated()
.and()
.apply(securityConfigurerAdapter());
}
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
}
private XAuthTokenConfigurer securityConfigurerAdapter() {
return new XAuthTokenConfigurer(userDetailsService, tokenProvider);
}
/**
* This allows SpEL support in Spring Data JPA @Query definitions.
*
* See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
*/
@Bean
EvaluationContextExtension securityExtension() {
return new EvaluationContextExtensionSupport() {
@Override
public String getExtensionId() {
return "security";
}
@Override
public SecurityExpressionRoot getRootObject() {
return new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};
}
};
}
}
And the respective AngularJS configuration:
'use strict';
angular.module('jhipsterApp')
.factory('AuthServerProvider', function loginService($http, localStorageService, Base64) {
return {
login: function(credentials) {
var data = "username=" + credentials.username + "&password="
+ credentials.password;
return $http.post('api/authenticate', data, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
}
}).success(function (response) {
localStorageService.set('token', response);
return response;
});
},
logout: function() {
//Stateless API : No server logout
localStorageService.clearAll();
},
getToken: function () {
return localStorageService.get('token');
},
hasValidToken: function () {
var token = this.getToken();
return token && token.expires && token.expires > new Date().getTime();
}
};
});
authInterceptor:
.factory('authInterceptor', function ($rootScope, $q, $location, localStorageService) {
return {
// Add authorization token to headers
request: function (config) {
config.headers = config.headers || {};
var token = localStorageService.get('token');
if (token && token.expires && token.expires > new Date().getTime()) {
config.headers['x-auth-token'] = token.token;
}
return config;
}
};
})
Add authInterceptor to $httpProvider:
.config(function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
})
Hope this is helpful!
This video from SpringDeveloper channel may be useful too: Great single page apps need great backends. It talks about some best practices (including session management) and demos working code examples.
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