First of all, let's import some dependencies
Before starting the real implementation, please, try to get these dependencies on your project:
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
compile group: 'com.auth0', name: 'java-jwt', version: '3.10.3'
compile group: 'org.springframework.security', name: 'spring-security-core', version: '5.1.5.RELEASE'
compile group: 'org.springframework.security', name: 'spring-security-web', version: '5.1.5.RELEASE'
compile group: 'org.springframework.security', name: 'spring-security-config', version: '5.1.5.RELEASE'
You might have the basic packages to build an API like Spring Starter Web, if you don't know how to build an API with java and Spring Boot, please, read the following article: Building a Simple API with Java and Spring Boot
And make sure you already have your UserRepository Implemented, but if you don't know how to implement a simple connection between java and any SQL database with H2, please, read the following article: Implementing a Simple Database with Java, JPA, Hibernate and SQL
Creating a Bean to our PasswordEncoder
If you don't know what is a Bean, please, read the following article: What is a Java @Bean?
Please, at your main class, paste the following code:
@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
It'll provide a BCryptPasswordEncoder instance to us.
Implemeneting our own UserDetailsService
So, assuming that we'll authenticate with a username and password, we have to implement the default class and method to search it in the database, right?
Follow bellow the code, please, stay alert to read the comments.
import com.bedigital.application.domain.ApplicationUser; | |
import com.bedigital.application.repositories.ApplicationUserRepository; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.security.core.userdetails.User; | |
import org.springframework.security.core.userdetails.UserDetails; | |
import org.springframework.security.core.userdetails.UserDetailsService; | |
import org.springframework.security.core.userdetails.UsernameNotFoundException; | |
import org.springframework.stereotype.Service; | |
import static java.util.Collections.emptyList; | |
// This service will be useful to us later, so first implement the | |
// UserDetailsService contract and let's implement its methods. | |
@Service | |
public class UserDetailsServiceImpl implements UserDetailsService { | |
@Autowired | |
// Remember I said that we would need an | |
// application user linked to the database | |
private ApplicationUserRepository applicationUserRepository; | |
// The default method permit us to search the user by username in the database | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
ApplicationUser user = applicationUserRepository.findByUsername(username.toLowerCase()); | |
if (user == null) { | |
throw new UsernameNotFoundException(username); | |
} | |
//We will wrap our credentials in the default java User Object | |
return new User(user.getUsername(), user.getPassword(), emptyList()); | |
} | |
} |
Let's implement the JWTAuthenticationFilter
First of all, our authentication will be a basic auth, where you provide a username and password and the system will verify if you are who you are supposed to be.
This class will rewrite some methods in a personal way to implement our UsernameAndPasswordAuthenticationFilter, which provides somethings like the answer to our auth.
Remember to be alert for the comments.
Follow below the code:
import com.auth0.jwt.JWT; | |
import com.bedigital.application.domain.ApplicationUser; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import org.springframework.security.authentication.AuthenticationManager; | |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
import org.springframework.security.core.Authentication; | |
import org.springframework.security.core.userdetails.User; | |
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | |
import javax.servlet.FilterChain; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.Date; | |
import static com.auth0.jwt.algorithms.Algorithm.HMAC512; | |
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { | |
private final AuthenticationManager authenticationManager; | |
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { | |
this.authenticationManager = authenticationManager; | |
} | |
@Override | |
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) { | |
//Do you remember when I said you should have already implemented | |
// a class where is your user with a username and password already | |
// linked to the database? So, we will use an instance of this | |
// class here. | |
try { | |
// As you can imagine here we get the body of the request | |
// (provided by req.getInputStream) and Instantiate our | |
// ApplicationUser (that has a username and password, something | |
// will be provided by the login post | |
ApplicationUser creds = new ObjectMapper() | |
.readValue(req.getInputStream(), ApplicationUser.class); | |
// Now we will authenticate with a default method in our authManager | |
// that we will override some methods later to implement a specific authentication method | |
return authenticationManager.authenticate( | |
new UsernamePasswordAuthenticationToken( | |
creds.getUsername(), | |
creds.getPassword(), | |
new ArrayList<>()) | |
); | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
// Let's reimplement the result of our authentication when successful | |
@Override | |
protected void successfulAuthentication(HttpServletRequest req, | |
HttpServletResponse res, | |
FilterChain chain, | |
Authentication auth) throws IOException { | |
// To access our system we'll use a JWT (Json Web Token) that will provide | |
// to us some data like time expiration (you can use it to define how long | |
// the client can use your system with that token, furthemore we'll have | |
// data like, password encrypted, username and a payload (like a body) if you need | |
// See the SecurityConstants ? It's a class that I created, but if you wanna | |
// put it directly to the code, feel yourself free. | |
// public static final long EXPIRATION_TIME = 480_000; // milliseconds | |
// public static final String HEADER_STRING = "Authorization"; | |
// public static final String SECRET = "oculos_apenas_99_reais"; | |
// public static final String TOKEN_PREFIX = "Bearer "; // I added a space after the prefix purposefully | |
// public static final String SIGN_UP_URL = "/sign-up"; | |
String token = JWT.create() | |
.withSubject(((User) auth.getPrincipal()).getUsername()) | |
.withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME)) | |
.sign(HMAC512(SecurityConstants.SECRET.getBytes())); | |
//We could finish our application here and you would get the token in your header | |
res.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token); | |
String body = SecurityConstants.TOKEN_PREFIX + token; | |
//But I would like to return it in our response body | |
res.getWriter().write(body); | |
res.getWriter().flush(); | |
} | |
} |
That is our AuthenticationFilter, responsible to verify the username and password data (we can say that it is the class that execute the "login")
Lets Implement the JWTAuthorizationFilter
If the Authentication Filter verifies and confirms the data, our Authorization Filter is responsible for the request, just like verify our token and show the authorities.
Remember to be alert for the comments.
Follow Below the file:
Finally, the WebSecurity Class
This is the class that interacts with the web layer, here we also have the cors configuration, allows, signup redirect, we define the login endpoint, etc etc etc.
Remember to be alert for the comments.
Follow below de the file:
package com.bedigital.application.security; | |
import com.bedigital.application.services.security.UserDetailsServiceImpl; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.http.HttpMethod; | |
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | |
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | |
import org.springframework.security.config.http.SessionCreationPolicy; | |
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | |
import org.springframework.web.cors.CorsConfiguration; | |
import org.springframework.web.cors.CorsConfigurationSource; | |
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | |
@EnableWebSecurity | |
@Configuration | |
public class WebSecurity extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private UserDetailsServiceImpl userDetailsServiceImpl; | |
@Autowired | |
private BCryptPasswordEncoder bCryptPasswordEncoder; // do you remember we created a bean for that? here is the usage | |
@Override | |
//here are some configurations, let's explain | |
protected void configure(HttpSecurity http) throws Exception { | |
http.headers().frameOptions().sameOrigin() // we accpet all frames that are of the same origin (I recommend you disable | |
// the frameoptions | |
.and() | |
.cors().configurationSource(corsConfigurationSource()) // you can scroll down and read the cors config | |
.and() | |
.csrf().disable() // we are disabling the csrf | |
// (Because we used JWT, and it's a type of token with something with cookies) | |
.authorizeRequests() | |
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll() // we are liberating the sign up url | |
// to post opreations without be logged in | |
.antMatchers("/h2-console/**").permitAll() // here we also took as free the h2 url, because sometimes we need to verify | |
// the data | |
.anyRequest().authenticated() // here we say that any other request need to be authenticated | |
.and() | |
.addFilter(new JWTAuthenticationFilter(authenticationManager())) // our filters are here | |
.addFilter(new JWTAuthorizationFilter(authenticationManager())) // and our auth | |
// this disables session creation on Spring Security | |
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // we dont work with sessions, just jwt | |
} | |
@Override | |
public void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder); // here we show the class who will get | |
// the user info, and the encoder | |
} | |
@Bean | |
CorsConfigurationSource corsConfigurationSource() { | |
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | |
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); | |
source.registerCorsConfiguration("/**", corsConfiguration); // we permit any url after / execute http operations with our system | |
return source; | |
} | |
} |
We can just finish here, but, as a bonus, there is a test class to test your login functionality.
#Bonus - Test Impl
package com.bedigital.application.resources; | |
import com.bedigital.application.domain.ApplicationUser; | |
import com.bedigital.application.repositories.ApplicationUserRepository; | |
import com.bedigital.application.services.ApplicationUserService; | |
import com.google.gson.Gson; | |
import com.google.gson.GsonBuilder; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.Test; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | |
import org.springframework.boot.test.context.SpringBootTest; | |
import org.springframework.test.web.servlet.MockMvc; | |
import org.springframework.test.web.servlet.MvcResult; | |
import static org.assertj.core.api.Assertions.assertThat; | |
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | |
@SpringBootTest | |
@AutoConfigureMockMvc | |
public class AuthenticationTest { | |
private static final ApplicationUser APPLICATION_USER = new ApplicationUser("eddie", "1234"); | |
private static final Gson GSON = new GsonBuilder().create(); | |
@Autowired | |
private ApplicationUserService applicationUserService; | |
@Autowired | |
private MockMvc mockMvc; | |
@BeforeEach // Usually the Junit executes a rollback (undo the done operations), then, because of that we insert an user after | |
// each operation, IT'S WRONG, you should insert as a BeforeClass and write a TestInstance(PER_CLASS) annotation upon the class | |
// but I don't want to refactor this test, if you add more than one method it will break so easily as it can | |
public void insertData() { | |
applicationUserService.save(APPLICATION_USER); | |
} | |
@Test | |
public void shouldAuthenticate() throws Exception { | |
String userJSON = GSON.toJson(new ApplicationUser("eddie", "1234")); // I am using the GSON lib provided by google to | |
// work with json conversions | |
MvcResult mvcResult = mockMvc.perform(post("/login").content(userJSON)).andReturn(); // the mockMvc literally will mock a | |
// request to our server | |
String bearer = mvcResult.getResponse().getContentAsString(); // if everything goes okay, we will receive an unempty string | |
assertThat(!bearer.isEmpty()); // just validate :D | |
} | |
} |
THANK YOU!!! ENJOY THE ARTICLE <3, LEAVE YOUR LIKE HERE SWEET!!
Top comments (0)