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.

Why Do We Need Spring Security?

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.

Spring Security in Action

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:

  1. Username and password verification
  2. Role control
  3. Token distribution, using JWT
  4. Token verification
  5. Password crypto

Through the process of implementation, we will cover some fundamental principles of Spring Security.

Include Spring Security Dependencies

compile "org.springframework.boot:spring-boot-starter-security"

Token Distribution

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:

  1. Token store
  2. Token converter
  3. Token manager
  4. Enhancer chain

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.

Token Verification

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:

  1. The passwordEncoder() function defines the way to encode and compare the passwords.
  2. The 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

}

  1. The 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;

}

  1. The authenticationManager() function defines the token verification logic. This class will be used to check user authentication when a token is refreshed.

Optional: Custom Token Authorization Verification Logic

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());

}

Optional: Custom Verification Chain

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

UserDetailServiceis the core interface that loads user-specific data. We must realize the loadUserByUsername() function to locate the user and the user’s role.

REST APIs

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);

}

Demo

1. Apply for a Token

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"}

2. Use the Token in Role Control

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"}]}]

3. Verify the Token With JWT

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

Spring Security in Action
2 Likes20.30 GEEK