Authentication using JWT & Role-Based Access Control in Spring Boot & MYSQL (II)

This is the final part of Authentication using JWT & Role-Based Access Control in Spring Boot & MYSQL

In the controllers folder, create UserController.java file to define routes for login and registration APIs.

package com.dev.springtutorial.controllers;


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dev.springtutorial.auth.jwt.JwtUtils;
import com.dev.springtutorial.auth.payload.Request.LoginRequest;
import com.dev.springtutorial.auth.payload.Request.RegisterRequest;
import com.dev.springtutorial.auth.payload.Response.JwtResponse;
import com.dev.springtutorial.auth.payload.Response.MessageResponse;
import com.dev.springtutorial.auth.services.MyUserDetails;
import com.dev.springtutorial.models.ERole;
import com.dev.springtutorial.models.Role;
import com.dev.springtutorial.models.User;
import com.dev.springtutorial.repos.RoleRepository;
import com.dev.springtutorial.repos.UserRepository;
// allow APIs access from client apps usig diffeent domains
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")

public class UserController {
	@Autowired
	AuthenticationManager authenticationManager;

	@Autowired
	UserRepository userRepository;

	@Autowired
	RoleRepository roleRepository;

	@Autowired
	PasswordEncoder encoder;

	@Autowired
	JwtUtils jwtUtils;

	@PostMapping("/login")
	public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
		// authenticate the user
		Authentication authentication = authenticationManager.authenticate(
				new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
		// set authentication to current security context
		SecurityContextHolder.getContext().setAuthentication(authentication);
		// create token from the authentication
		String jwt = jwtUtils.generateJwtToken(authentication);
		// get user details from the authentication
		MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();		
		// create role list
		Collection<? extends GrantedAuthority> authorities=userDetails.getAuthorities();
        List<String> roles= new ArrayList<>();
		for (GrantedAuthority grantedAuthority : authorities) {
            roles.add(grantedAuthority.getAuthority());
        }
	
		// return jwt response
		return ResponseEntity.ok(new JwtResponse(jwt, 
												 userDetails.getId(), 
												 userDetails.getUsername(), 
												 userDetails.getEmail(), 
												 roles));
	}

	@PostMapping("/register")
	public ResponseEntity<?> registerUser(@Valid @RequestBody RegisterRequest signUpRequest) {
		// check if the user already exists
		if (userRepository.existsByUsername(signUpRequest.getUsername())) {
			return ResponseEntity
					.badRequest()
					.body(new MessageResponse("Error: Username already exists!"));
		}

		if (userRepository.existsByEmail(signUpRequest.getEmail())) {
			return ResponseEntity
					.badRequest()
					.body(new MessageResponse("Error: Email is already in use!"));
		}

		// Create new user's account
		User user = new User(signUpRequest.getUsername(), 
							 signUpRequest.getEmail(),
							 encoder.encode(signUpRequest.getPassword()));
		
		Set<String> strRoles = signUpRequest.getRole();
		Set<Role> roles = new HashSet<>();

		if (strRoles == null) {
			Role userRole = roleRepository.findByName(ERole.ROLE_USER)
					.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
			roles.add(userRole);
		} 
		else {
			strRoles.forEach(role -> {
				switch (role) {
				case "admin":
					Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
							.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
					roles.add(adminRole);

					break;
				default:
					Role userRole = roleRepository.findByName(ERole.ROLE_USER)
							.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
					roles.add(userRole);
				}
			});
		}

		user.setRoles(roles);
		userRepository.save(user);

		return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
	}
}

In the springtutorial folder, add security configuration file to configure Spring security in the system. In the configuration file, we define token filter to verify the token, authentication manager to process authentication request, filter security chains to secure request urls, and password encoder to specify bcrypt password encode. The prePostEnabled parameter  of @EnableGlobalMethodSecurity method must be set to true to use @PreAuthorized annotation to define role-based access on APIs.

springtutorial/WebSecurityConfig.java
package com.dev.springtutorial;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.dev.springtutorial.auth.jwt.AuthTokenFilter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity

@EnableGlobalMethodSecurity(

		prePostEnabled = true) // to use @PreAuthorize annotation
public class WebSecurityConfig { // specify which token filter to be used @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } //The SecurityFilterChain bean defines which URL paths should be secured and which should not @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { //Disables the AbstractHttpConfigurer by removing it. After doing so a fresh version of the configuration can be applied. http.cors().and().csrf().disable(); //Spring Security will never create an HttpSession and it will never use it to obtain the SecurityContext http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // URL paths should be secured and which not http .authorizeHttpRequests((requests) -> requests .antMatchers("/", "/home").permitAll() .antMatchers("/api/auth/**").permitAll() .antMatchers("/image/**").permitAll() .antMatchers("/uploads/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } // use bcrypt password encoder @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

Execute the queries below to add sample data to roles table in MYSQL database:
 
INSERT INTO roles(name) VALUES ('ROLE_USER');
INSERT INTO roles(name) VALUES ('ROLE_ADMIN');

To apply role-based access control, we add @PreAuthorize annotation to the APIs handler mehtods. The products/all route is accessible to a logged user that has ROLE_USER or ROLE_ADMIN. 


 @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
  public @ResponseBody Iterable<Product> getAllProducts() {
    .....................
}

We allow product inserting, updating, and deleting to admin user only.

@PostMapping("/add") // POST Requests
@PreAuthorize("hasRole('ROLE_ADMIN')")
  public @ResponseBody Product addPro ( ){
...............
}

@PutMapping("/update/{id}") // PUT Requests
@PreAuthorize("hasRole('ROLE_ADMIN')")
 public @ResponseBody Product updatePro (){
.......

@DeleteMapping("/delete/{id}") // Map ONLY POST Requests
@PreAuthorize("hasRole('ROLE_ADMIN')")
 public @ResponseBody void deletePro (@PathVariable int id) {
...................
}


While MYSQL is running, run the project. To register a new account, access http://localhost8080/api/auth/register. Then login via http://localhost8080/api/auth/login.

In the next tutorial, you will learn to create React front-end app to access the Spring Boot APIs.

Comments