Spring Security in Action - Spring Security is a highly customizable authentication and access-control framework for Java applications, especially for Spring-based applications. This framework is also widely used in Java development.
Spring Security is integrated with the most popular framework, Spring Boot. It supports both authentication and authorization, which are the most popular approaches to dealing with security issues between the server and client-end. Like all Spring-based projects, the real power of Spring Security is found in how easily it can be extended to meet customer requirements.
In this example, we will go through a very basic Spring Security application. There are four important classes to be introduced — HttpSecurity
, WebSecurityConfigurer
, UserDetailsService
, and AuthenticationManager
. The final application will cover the following features:
Through the process of implementation, we will cover some fundamental principles of Spring Security.
compile "org.springframework.boot:spring-boot-starter-security"
The first thing to do is define a way to grant tokens. There are several grant types to accomplish this, including “password
,” " authorization<em>code," " implicit," and " client</em>credentials
." In our example, we used " password
" as the grant type.
@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()//jdbc()
.withClient(clientId)
.secret(passwordEncoder.encode(clientSecret))
.authorizedGrantTypes("password")
.scopes("read", "write")
.resourceIds(resourceIds);
}
Normally, we don’t use in-memory client id and secret. In production, we can configure to read all clients from the database, as shown below:
configurer.jdbc()
After that, we need to configure token settings, including:
In this example, we are using JWT to manage our tokens. JWT provides convenient APIs that closely integrate with Spring Security. There is a converter used to convert and decode tokens. All you need to do is to set a signing key and then set them in the config(AuthorizationServerEndpointsConfigurer endpoints)
function.
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}
Remember that the tokenService
and authenticationManager
must be the same one in token verification so that the token can be decoded properly.
Now, you are able to implement your own security policy. A popular way is to extend WebSecurityConfigurerAdapter
and rewrite security control functions based on your customer’s requirements. Listed below are the important functions:
passwordEncoder()
function defines the way to encode and compare the passwords.configure(HttpSecurity http)
function sets the resource strategy.@Override
public void configure(HttpSecurity http) throws Exception {
//@formatter:off
http
.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll() //no authorization required here
.antMatchers("/demo/**").authenticated(); //need authorization
//@formatter:on
}
configure(ResourceServerSecurityConfigurer resources)
function defines the security strategy.In this config()
function, we need to assign a token service to explain tokens. A typical token service is defined below.
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
authenticationManager()
function defines the token verification logic. This class will be used to check user authentication when a token is refreshed.If the default token manager does not meet your requirements, which is happening all the time, you could use your own authenticationProvider
by using the code below:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jwtAuthenticationProvider());
}
And the provider can look like this:
@Override
public Authentication authenticate(Authentication authentication) throws
AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken();
//I want a token never expired.
//if (jwt.getExpiresAt().before(Calendar.getInstance().getTime()))
//throw new NonceExpiredException("Token expires");
String clientId = jwt.getSubject();
UserDetails user = userService.loadUserByUsername(clientId);
if (user == null || user.getPassword() == null)
throw new NonceExpiredException("Token expires");
Algorithm algorithm = Algorithm.HMAC256(user.getPassword());
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(clientId)
.build();
try {
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
return new JwtAuthenticationToken(user, jwt, user.getAuthorities());
}
If the authenticate()
function throws an exception, we may handle it or just record it in the database. To implement it, we can just set tokenValidSuccessHandler
and tokenValidFailureHandler
. In the handler, you can rewrite onAuthenticationSuccess
and onAuthenticationFailure
with your own logic.
...
.and()
.apply(new MyValidateConfigure<>())
.tokenValidSuccessHandler(myVerifySuccessHandler())
.tokenValidFailureHandler(myVerifyFailureHandler())
...
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponseresponse,
AuthenticationException exception) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}
UserDetailService
is the core interface that loads user-specific data. We must realize the loadUserByUsername()
function to locate the user and the user’s role.
Once all the authorization configurations have been finished, you can start to enjoy developing your project. To control your role and access, you can simply add @PreAuthorize("hasAuthority('ADMIN_USER')")
in your REST API declaration.
@RequestMapping(value = "/users", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('ADMIN_USER')") //user with ADMIN_USER role have this access.
public ResponseEntity<List<User>> getUsers() {
return new ResponseEntity<>(userService.findAllUsers(), HttpStatus.OK);
}
Client-end can either use postman or curl to get a token.
curl client-id:client-password@localhost:8080/oauth/token -d grant_type=password -d username=admin.admin -d password=Test!123
You will get:
{"access_token":"eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsic3ByaW5nLXNlY3VyaXR5LWRlbW8tcmVzb3VyY2UtaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTU0ODQ0NTQxLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiI4MTM3Y2Q4OS0wMWMyLTRkMTgtYjA4YS05MjNkOTcxYjNhYzQiLCJjbGllbnRfaWQiOiJjbGllbnQtaWQifQ.1t_4xVT8xaAtisHaNT_nMRBLKfpiI0SZQ2bbEGxu6mk","token_type":"bearer","expires_in":43199,"scope":"read write","jti":"8137cd89-01c2-4d18-b08a-923d971b3ac4"}
Then use the token above to post a request which needs authorization.
curl http://localhost:8080/demo/users -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsic3ByaW5nLXNlY3VyaXR5LWRlbW8tcmVzb3VyY2UtaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTU0ODQ0NTQxLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiI4MTM3Y2Q4OS0wMWMyLTRkMTgtYjA4YS05MjNkOTcxYjNhYzQiLCJjbGllbnRfaWQiOiJjbGllbnQtaWQifQ.1t_4xVT8xaAtisHaNT_nMRBLKfpiI0SZQ2bbEGxu6mk"
It will return:
[{"id":1,"username":"jakob.he","firstName":"Jakob","lastName":"He","roles":[{"id":1,"roleName":"STANDARD_USER","description":"Standard User"}]},{"id":2,"username":"admin.admin","firstName":"Admin","lastName":"Admin","roles":[{"id":1,"roleName":"STANDARD_USER","description":"Standard User"},{"id":2,"roleName":"ADMIN_USER","description":"Admin User"}]}]
Put your token and signing key in jwt.io; you will get the following result.
Let’s quickly go over what we have done: We have introduced Spring Security and why we need to use it. We have also implemented a complete Spring Security application that included token management, token distribution, and REST APIs that are required for web authorization.
I hope this article is helpful!
#spring-boot #security