Find Client's Location In Spring Boot with IP2Location and Update via Scheduling
In this tutorial, we are going to find location of your clients using spring boot and IP2Location and also we will look at how to update IP2Location …
In this post, we are going to setup spring boot rest project with using JWT. we will also integrate the our spring boot application with the frontend (in this case I am going to use VueJS).
If you only need to see the code, here is the github link
Please create a new spring boot project with the following dependencies:
You must change the postgresql configuration inside the application.properties
file as your needs.
Change the url, username and password
spring.datasource.url=jdbc:postgresql://localhost:5432/testdatabase
spring.datasource.username=postgres
spring.datasource.password=1234
# For localdb, it would be good to re-create all-tables,
# because ddl-auto = update, for the first run, all tables will be created automatically
spring.jpa.hibernate.ddl-auto=update
logging.level.org.springframework.security=DEBUG
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.
You should use JWT for:
JWT has its own structure but i am not going into detail of it. You may look at the https://jwt.io/introduction
In the project, because we will use our database, then we need to implement our userDetailsService via implementing UserDetailsService
interface.
As I said my previous post, If we create our UserDetailsService, we must also create a PasswordEncoder.
I am going to use
BCryptPasswordEncoder
Because we have two different origins (one for running on localhost:8080-spring boot-, and one for running on localhost:8081-vuejs-), we should enable cross origin request between these origins. Otherwise client ajax request (I will use axios) will fail. For the sake of simplicity, I allowed all the origins:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList(CorsConfiguration.ALL));
corsConfiguration.setAllowedMethods(Collections.singletonList(CorsConfiguration.ALL));
corsConfiguration.setAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL));
corsConfiguration.setMaxAge(Duration.ofMinutes(10));
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
// ...
}
UserDetailsService will only get the username from the authentication provider and send it to the userRepository
to get one User from database.
public class MyUserDetailService implements UserDetailsService {
private final UserRepository userRepository;
public MyUserDetailService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserDTO> userInDb = userRepository.findByUsername(username);
UserDTO userDTO = userInDb.orElseThrow(() -> new UsernameNotFoundException("Not Found in DB"));
return new SecureUser(userDTO);
}
}
If repository returns the User from the database, we should wrap it with new object. And this new object must extend the UserDetails
. Otherwise spring security can not know authenticated User. That is the reason why I created a new class called SecureUser
:
public class SecureUser implements UserDetails {
private List<SimpleGrantedAuthority> userRoles;
private String password;
private String username;
public SecureUser(UserDTO userDTO) {
this.userRoles = new ArrayList<>();
userDTO.getUserRoles().forEach(userRoleDTO -> this.userRoles.add(new SimpleGrantedAuthority(userRoleDTO.getRole())));
this.password = userDTO.getPassword();
this.username = userDTO.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.userRoles;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
// ...
}
Because we created MyUserDetailService
as a Service
bean, Spring will automatically use our UserDetailsService
We also must override the default configuration when we create custom UserDetailsService. This step is just to creating a bean in the configuration class. Here is the security configuration:
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsService userDetailsService() {
return new MyUserDetailsService(userRepository);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
If you run the project in this setup, your all requests will return “Unauthorized” error message with 401 status code. Because Spring Security protects all the endpoints. In other words it means, we aren’t authenticated.
$ curl -X GET http://localhost:8080
Response:
{
"timestamp": "2021-07-10T15:59:56.462+00:00",
"status": 401,
"error": "Unauthorized",
"message": "",
"path": "/"
}
We must adjust some endpoints to pass spring security, These endpoints could be login and register endpoints
We should switch off the default web application security configuration and enable the our security configuration. We can achieve adding @EnableWebSecuirty
annotation and extending with WebSecurityConfigurerAdapter
After that we need to specify which endpoint(s) should not be protected by Spring Security. Here is the SecurityConfiguration:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private String[] allowedEndpoints() {
return new String[] {
"/api/login",
"/api/register"
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(allowedEndpoints()).permitAll()
.anyRequest().authenticated();
}
}
Let’s send it the same request:
$ curl -X GET http://localhost:8080
{
"timestamp": "2021-07-10T17:37:19.070+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/"
}
Basically it means that we are authenticated (in this case Anonymous) but Anonymous user has no permission to send request to the endpoint http://localhost:8080
To test the login and register endpoint, you may create a dummy methods:
In the sake of simplicity, I just annotated these endpoints as GET requests.
@RestController
@RequestMapping("/api")
public class LoginController {
@GetMapping("/login")
public ResponseEntity<?> doRegister() {
return ResponseEntity.ok("login endpoint");
}
}
@RestController
@RequestMapping("/api")
public class RegisterController {
@GetMapping("/register")
public ResponseEntity<?> doRegister() {
return ResponseEntity.ok("register endpoint");
}
}
Now send request to the login or register endpoint:
$ curl -X GET http://localhost:8080/api/register
Response:
register endpoint
We got a response, because we authenticated as AnonymousUser, also we have a permission to send request /api/login
& /api/register
We should first sign-up new user. Therefore we need to implement our register endpoint.
Let’s summarize the tasks when someone hits the register endpoints. The register endpoint have to perform the following actions:
RegisterService
ResponseEntity
This step will be easy with annotations
public class RegisterRequest {
@NotBlank(message = "Username can not be empty")
private String username;
@NotBlank(message = "Password can not be empty")
private String password;
// getters and setters...
}
@RestController
@RequestMapping("/api")
public class RegisterController {
@PostMapping("/register")
public ResponseEntity<?> doRegister(@RequestBody @Valid RegisterRequest registerRequest) {
return ResponseEntity.ok("register endpoint");
}
}
Simple:
@RestController
@RequestMapping("/api")
public class RegisterController {
private final RegisterService registerService;
public RegisterController(@Autowired RegisterService registerService) {
this.registerService = registerService;
}
@PostMapping("/register")
public ResponseEntity<?> doRegister(@RequestBody @Valid RegisterRequest registerRequest) {
RegisterResponse registerResponse = registerService.doRegister(registerRequest);
return ResponseEntity.ok(registerResponse);
}
}
The register service have to perform the following actions:
First it checks the username, if the username is in our database, it throws an exception to indicate this user has already registered.
It will encrypt the user password with the current password encoder (in our example it will be BCryptPasswordEncoder)
I believe code is self-explanatory.
If you want to return specific response when user is not found, you should wait (or search on the internet) for my post related to that.
@Service
public class RegisterServiceImpl implements RegisterService {
private final PasswordEncoder passwordEncoder;
private final RegisterRepository registerRepository;
public RegisterServiceImpl(@Autowired PasswordEncoder passwordEncoder, @Autowired RegisterRepository registerRepository) {
this.passwordEncoder = passwordEncoder;
this.registerRepository = registerRepository;
}
@Transactional
@Override
public RegisterResponse doRegister(RegisterRequest registerRequest) {
Optional<UserDTO> isUserInDb = registerRepository.findByUsername(registerRequest.getUsername());
if (isUserInDb.isPresent()) {
throw new RuntimeException("User has already in db");
}
// ...
}
}
@Service
public class RegisterServiceImpl implements RegisterService {
// ...
@Transactional
@Override
public RegisterResponse doRegister(RegisterRequest registerRequest) {
// ...
UserDTO newUser = mapNewRegisterToNewUser(registerRequest);
registerRepository.save(newUser);
RegisterResponse registerResponse = new RegisterResponse();
registerResponse.setRegistered(true);
registerResponse.setMesssage("User was saved");
return registerResponse;
}
private UserDTO mapNewRegisterToNewUser(RegisterRequest registerRequest) {
UserDTO userDTO = new UserDTO();
userDTO.setUsername(registerRequest.getUsername());
userDTO.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
UserRoleDTO basicRole = new UserRoleDTO();
basicRole.setRole("ROL_BASIC");
basicRole.setUserDTO(userDTO);
userDTO.getUserRoles().add(basicRole);
return userDTO;
}
}
Let’s try to send a request:
$ curl -X POST -H "Content-Type: application/json" -d '{ "username": "test", "password": "1234"}' http://localhost:8080/api/register
Response will be:
{ "messsage": "User was saved", "registered": true }
Take a look at the database:
testdatabase=# select * from users;
-[ RECORD 1 ]----------------------------------------------------------
id | 1
password | $2a$10$IA3OlClwnzWxcxb2wXujhuZS/bMdkXga7yhzFswiN2UaUOlJpksAC
username | test
As you can see, we saved the user in the database.
If you send the same request, you will get a response like this:
{
"timestamp": "2021-07-10T19:35:13.141+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/api/register"
}
Look at the console:
java.lang.RuntimeException: User has already in db
at com.mehmetozanguven.springbootjwtexample.service.register.RegisterServiceImpl.doRegister(RegisterServiceImpl.java:30) ~[classes/:na]
The login endpoint have to perform the following actions:
LoginService
ResponseEntity
Again!! This step will be easy with annotations
public class LoginRequest {
@NotBlank(message = "Username can not be empty")
private String username;
@NotBlank(message = "Password can not be empty")
private String password;
// getters and setters ..
}
@RestController
@RequestMapping("/api")
public class LoginController {
@PostMapping("/login")
public ResponseEntity<?> doLogin(@RequestBody @Valid LoginRequest loginRequest) {
// ...
}
}
@RestController
@RequestMapping("/api")
public class LoginController {
private final LoginService loginService;
public LoginController(@Autowired LoginService loginService) {
this.loginService = loginService;
}
@PostMapping("/login")
public ResponseEntity<?> doLogin(@RequestBody @Valid LoginRequest loginRequest) {
LoginResponse loginResponse = loginService.doLogin(loginRequest);
return ResponseEntity.ok(loginResponse);
}
}
The login service have to perform the following actions:
AuthenticationManager should return fully authenticated instance or throw an error.
AuthenticationManager
We should first create an AuthenticationManager bean. For that we can use default authenticationManager object as bean:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// ...
}
import org.springframework.security.authentication.AuthenticationManager;
// ...
@Service
public class LoginServiceImpl implements LoginService {
private final AuthenticationManager authenticationManager;
public LoginServiceImpl(@Autowired AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public LoginResponse doLogin(LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
}
}
If you recall from previous blogs, AuthenticationManager
will find the correct provider with the given Authentication
. In this case we provided UsernamePasswordAuthenticationToken
.
Here is the step by step what is happening:
AuthenticationManager
will try to find AuthenticationProvider
for the Authentication
instance.UsernamePasswordAuthenticationToken
, provider will be DaoAuthenticationProvider
AuthenticationManager
will call Authentication authentication = provider.authenticate()
method.UserDetailsService
and PasswordEncoder
. DaoAuthenticationProvider
will use these beans.UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
this.getUserDetailsService()
equals to ourUserDetailsService(MyUserDetailService)
. After allMyUserDetailService#loadUserByUsername
will be called by the provider. And we will returnUserDetails
or throw an error
UserDetails
, provider will check the request’s password and password from the database. Provider will use our password encoder for that (BCryptPasswordEncoder
)protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// userDetails = MyUserDetailService#loadUserByUsername
// authentication.getCredentials() = loginRequest.getPassword()
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
Spring will store the authenticated object in the SecurityContext
. You need this setup, if you want to get authenticated user anywhere in the code. Thus we should manually set the authenticated object.
@Service
public class LoginServiceImpl implements LoginService {
// ...
@Override
public LoginResponse doLogin(LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
// ...
}
}
We only need to find authenticated username and generating JWT for the future requests. (User will send the jwt in the header)
I also created JwtUtil class to validate and generate. But I won’t explain how it works. You can search on the Internet.
@Service
public class LoginServiceImpl implements LoginService {
// ...
@Override
public LoginResponse doLogin(LoginRequest loginRequest) {
// ...
SecureUser userDetails = (SecureUser) authentication.getPrincipal();
String jwtToken = JwtUtil.generateJwtToken(userDetails.getUsername());
// ...
}
}
Response from the login service will only include JWT.
@Service
public class LoginServiceImpl implements LoginService {
// ...
@Override
public LoginResponse doLogin(LoginRequest loginRequest) {
// ...
LoginResponse loginResponse = new LoginResponse();
loginResponse.setJwtToken(jwtToken);
return loginResponse;
}
}
Our login endpoint is ready to test. Send this request:
$ curl -X POST -H "Content-Type: application/json" -d '{ "username": "test", "password": "1234"}' http://localhost:8080/api/login | jq .
Response:
{
"jwtToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjI1OTUxMDczLCJleHAiOjE2MjU5NTQ2NzN9.wJq_Vu0ekWq02tKERNV5aEIiueKUA76UodLQVumqOPZXH_ZDlMGluE_yXJFltjjPF7-H83yVadozoiOfH0zfBg"
}
For invalid credentials:
$ curl -X POST -H "Content-Type: application/json" -d '{ "username": "test", "password": "12345"}' http://localhost:8080/api/login | jq .
{
"timestamp": "2021-07-10T21:05:25.958+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/api/login"
}
Look at the console:
// ...
org.springframework.security.authentication.BadCredentialsException: Bad credentials
at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks
// ...
After successful login, we should check the JWT for each subsequent request to verify the user. We somehow intercepts the all coming requests. This can be done by adding filter in our application. Let’s implement this feature.
This filter will have the following responsibilities:
Spring already provides a base class to intercept all requests which is OncePerRequestFilter
. We need only to extend this class.
public class AuthTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return request.getServletPath().equals("/api/login") ||
request.getServletPath().equals("/api/register");
}
}
JWT will be send in the Header with the following format:
Authorization: Bearer {JWT}
public class AuthTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request)
} catch (Exception e) {
logger.error("Exception: ", e);
}
filterChain.doFilter(request, response);
}
// ...
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
I will use the method from JwtUtil
to verify.
public class AuthTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && JwtUtil.validateJwtToken(jwt)) {
// JWT valid
}
} catch (Exception e) {
logger.error("Exception: ", e);
}
filterChain.doFilter(request, response);
}
}
In this time, we don’t need to call authenticationManager. Using the UserDetailsService will be enough:
public class AuthTokenFilter extends OncePerRequestFilter {
private final UserDetailsService myUserDetailsService;
public AuthTokenFilter(UserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// ...
if (jwt != null && JwtUtil.validateJwtToken(jwt)) {
String username = JwtUtil.getUsernameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// ...
}
}
After getting users from database with the UserDetails
type:
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
Make sure that called the filterChain.doFilter(request, response);
at the end.
public class AuthTokenFilter extends OncePerRequestFilter {
// ...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// ...
} catch (Exception e) {
// ...
}
filterChain.doFilter(request, response);
}
}
We can add filter before UsernamePasswordAuthenticationFilter.class
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
addFilterBefore
In the Spring environment we have three options to put filter:
http.addFilter()
:Adds a Filter
that must be an instance of or extend one of the Filters provided within the Security framework. The method ensures that the ordering of the Filters is automatically taken care of. In other words, you can not use this method to add your custom filter. In our project, AuthTokenFilter
is a custom filter and not extending any of the filters provided by Spring Security. Some of the known(or provided) filters: ChannelProcessingFilter, SecurityContextPersistenceFilter, LogoutFilter, SessionManagementFilter...
All list can be found in this link: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/HttpSecurityBuilder.html#addFilter(javax.servlet.Filter)
http.addFilterBefore
:Allows adding a Filter
before one of the known Filter
classes. The known Filter
instances are either a Filter
listed in addFilter(filter)
(click the link above) or a Filter
that has already been added using addFilterAfter
or addFilterBefore
In other words, we are free to insert our custom filter.
http.addFilterAfter
:Allows adding a Filter
before one of the known Filter
classes. The known Filter
instances are either a Filter
listed in addFilter(filter)
(click the link above) or a Filter
that has already been added using addFilterAfter
or addFilterBefore
In other words, we are free to insert our custom filter.
The only part is to test this configuration with the protected resource(s)/controller(s). Let’s create dummy endpoint and test it.
This controller will return simple value to demonstrate only valid jwt can access it.
@RestController
@RequestMapping("/api")
public class ProtectedController {
@GetMapping("/protected")
public ResponseEntity<?> getProtectedResource() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
return ResponseEntity.ok("Hello Logged-In User:" + currentPrincipalName + ", you can access this resource, because JWT was valid");
}
}
First login into the system and get the JWT:
$ curl -X POST -H "Content-Type: application/json" -d '{ "username": "test", "password": "1234"}' http://localhost:8080/api/login
Response:
{
"jwtToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjI2MTAyMzk5LCJleHAiOjE2MjYxMDU5OTl9.wfvvukr9h5e7KS7YDqr3xFxv9iydlPXT8wW50zLaHWkTRF-gW_je_G7QwOWEshV1YCRrC9RQUViAsebksMI3CQ"
}
Now we can use this JWT in the header for the subsequent request(s):
$ curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer {{JWT}} " http://localhost:8080/api/protected
Hello Logged-In User:test, you can access this resource, because JWT was valid
With invalid JWT, you will get the response:
{
"timestamp": "2021-07-12T15:11:00.026+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/api/protected"
}
For invalid JWT, look at the console:
JwtUtil : Invalid JWT signature: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
For a final step, we can customize the default error response implementing AuthenticationEntryPoint
. We can do that in another post.
Unfortunately, there is really hard to explain front-end implementation step by step. Because I don’t have enough knowledge about front-end side. I will just give you a github link
In this tutorial, we are going to find location of your clients using spring boot and IP2Location and also we will look at how to update IP2Location …
In this post, let’s find out what is the CORS policy, how to implement in the Spring Boot and Spring Security, finally how to resolve most …