Sunday, December 14, 2025

Java spring notes13

 Handson

 

1. Implement JWT Authentication for Employee application you have created in yesterday handson

 

 

Spring Security

   - used to secure ur endpoints, who has privilege to access which endpoints

 

 

1. Add Spring security dependency (ie) spring-boot-starter-security, since we added security dependency by default security is enabled in ur appl

 

2. Start the appl, it will be provided with default password, run the appl http://localhost:5000/api/

  - It will by default open login page provided by spring security

       username: user

       password: paste generated pwd

 

3. Inorder to use our own username, pwd, we have to configure username and pwd in application.properties

 

spring.security.user.name=Ram

spring.security.user.password=abcd

 

4. Start the appl, run http://localhost:5000/api/, it will by default open login page where we have to provide username and pwd configured in application.properties

 

But this approach is not recommended approach, since we have hardcoded only one username and pwd in application.properties

 

5. If we want to configure multiple username and pwd with different roles, then we have to create separate class with @Configuration and @EnableWebSecurity annotation where we provide the logic for authentication and authorization

    Before Springboot3.0 version we have WebSecurityConfigureAdapter class which contains 2 overloaded  configure() method

1. configure(AuthenticationManagerBuilder a) which used for authentication purpose (ie) validate username and pwd

2. configure(HttpSecurity h) which used for authorization (ie) we provide privilege to user so that which user can access which endpoint

    But from Springboot3.0, this WebSecurityConfigureAdapter class is completely removed, then to perform authentication and authorization we have

1. UserDetailsService interface - used for authentication - 3 ways

a. In-memory authentication - we manually configure username and pwd along with their roles

b. Database authentication - fetch username and pwd from db

c. JWT Authentication

 

2. SecurityFilterChain interface - used for authorization

 

@Configuration

@EnableWebSecurity

public class SecurityConfig {

 

              //Authentication

              @Bean 

              public UserDetailsService userDetailsService(PasswordEncoder encoder) {

                             //In-Memory Authentication

                             UserDetails admin=User.withUsername("Ram")

                                                   .password(encoder.encode("abcd"))

                                                   .roles("ADMIN").build();

                             UserDetails user=User.withUsername("Sam")

                                                                                                       .password(encoder.encode("xyz"))

                                                  .roles("USER").build();

                             return new InMemoryUserDetailsManager(admin,user);

              }

             

              @Bean

              public PasswordEncoder passwordEncoder() {

                             return new BCryptPasswordEncoder();

              }

             

              

              //Authorization

              @Bean

              public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{

                             return http.csrf(csrf->csrf.disable())

                                  .authorizeHttpRequests(auth ->                                                                                                                                                                                                                                                 auth.requestMatchers("/api/welcome").permitAll()

                                      .anyRequest().authenticated())

                               .formLogin().and().build();

              }

}

 

6. Comment username and pwd in application.properties

 

7. Add mysql dependency in pom.xml

 

8. Start the appl and add movie info in table

 

http://localhost:5000/api/welcome - we can access without username and pwd

 

http://localhost:5000/api/movie - we can access only with username and pwd

 

http://localhost:5000/api/movie/100 - we can access only with username and pwd

 

9. Now both Ram and Sam can access all endpoints eventhough we have provided roles, now we want to restrict endpoints based on their roles for that we define @PreAuthorize annotation

 

@RestController

@RequestMapping("/api")

public class MovieController {

             

              @Autowired

              MovieService movieService;

 

              @GetMapping("/welcome")

              public String getMovieInfo() {

                             return "Welcome to Movie Application";

              }

             

              @PostMapping("/movie")

              @PreAuthorize("hasAuthority('ROLE_ADMIN')")

              public ResponseEntity<Movie> createMovie(@Validated @RequestBody Movie movie) {

                             Movie savedMovie=movieService.createMovie(movie);

                             return new ResponseEntity<Movie>(savedMovie,HttpStatus.CREATED);

              }

             

              @GetMapping("/movie")

              @PreAuthorize("hasAuthority('ROLE_ADMIN')")

              public ResponseEntity<List<Movie>> getAllMovies() {

                             List<Movie> movieList = movieService.getAllMovies();

                             if(movieList.isEmpty())

                                           return new ResponseEntity<>(HttpStatus.NO_CONTENT);

                             return new ResponseEntity<>(movieList,HttpStatus.CREATED);

              }

             

              @GetMapping("/movie/{id}")

              @PreAuthorize("hasAuthority('ROLE_USER')")

              public ResponseEntity<Movie> getMovieById(@PathVariable("id") Integer mid) {

                             Movie movie=movieService.getMovieById(mid);

                             return new ResponseEntity<>(movie,HttpStatus.CREATED);

              }

}

 

- By defining @PreAuthorize we cannot achieve authorization, we have to inform that we have implemented method level authorization using @EnableMethodSecurity in SecurityConfig class

 

@Configuration

@EnableWebSecurity

@EnableMethodSecurity

public class SecurityConfig {

 

}

 

10. Start the appl

 

http://localhost:5000/api/welcome - everyone can access without username and pwd

 

http://localhost:5000/api/movie - only Ram can access with username and pwd

 

http://localhost:5000/api/movie/100 - only Sam can access  with username and pwd

 

11. We have done SecurityConfig but we have hardcoded username and password so that we need in-memory authentication, but we need to fetch username and pwd from database

 

- Create entity class to store user info

 

@Entity

@Data

@AllArgsConstructor

@NoArgsConstructor

public class UserInfo {

  @Id

  private Integer id;

  private String name;

  private String email;

  private String password;

  private String role;

}

 

- Create repository interface

 

public interface UserInfoRepository extends JpaRepository<UserInfo, Integer>{

     Optional<UserInfo> findByName(String name);

}

 

12. We need to perform authentication by fetching username and pwd from db, so we have to comment in-memory authentication and create separate class called UserInfoUserDetailsService to perform db authentication

 

@Bean 

              public UserDetailsService userDetailsService(PasswordEncoder encoder) {

                             //In-Memory Authentication

                             /*UserDetails admin=User.withUsername("Ram")

                                                                        .password(encoder.encode("abcd"))

                                                                        .roles("ADMIN","MANAGER").build();

                             UserDetails user=User.withUsername("Sam")

                            .password(encoder.encode("xyz"))

                            .roles("USER").build();

                             return new InMemoryUserDetailsManager(admin,user);*/

                             return new UserInfoUserDetailsService();

              }

 

13. We create separate class called UserInfoUserDetailsService which implements UserDetailsService interface and override loadUserByUsername() which communicate with db and fetch username and pwd

 

@Component

public class UserInfoUserDetailsService implements UserDetailsService{

 

              @Autowired

              UserInfoRepository userRepo;

             

              @Override

              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

                             Optional<UserInfo> userInfo=userRepo.findByName(username);

                             return null;

              }

 

}

 

- loadUserByUsername() method returns UserDetails object (ie) username, pwd, roles, but whatever we are returning from db are UserInfo object, so we have to convert UserInfo object to UserDetails object, so for that we have to create separate class called UserInfoUserDetails which implements UserDetails interface

 

public class UserInfoUserDetails implements UserDetails{

             

              private String name;

              private String password;

              private List<GrantedAuthority> authorities;

             

              public UserInfoUserDetails(UserInfo userInfo) {

                             name=userInfo.getName();

                             password=userInfo.getPassword();

                             authorities=Arrays.stream(userInfo.getRole().split(","))

                                                                    .map(SimpleGrantedAuthority::new)

                                                                    .collect(Collectors.toList());

              }

 

              @Override

              public Collection<? extends GrantedAuthority> getAuthorities() {

                             // TODO Auto-generated method stub

                             return authorities;

              }

 

              @Override

              public String getPassword() {

                             // TODO Auto-generated method stub

                             return password;

              }

 

              @Override

              public String getUsername() {

                             // TODO Auto-generated method stub

                             return name;

              }

 

              @Override

              public boolean isAccountNonExpired() {

                             return true;

              }

             

              @Override

              public boolean isAccountNonLocked() {

                             return true;

              }

             

              @Override

              public boolean isCredentialsNonExpired() {

                             return true;

              }

             

              @Override

              public boolean isEnabled() {

                             return true;

              }

}

 

- We have to implement this conversion of UserInfo to UserDetails in loadUserByUsername()

 

@Component

public class UserInfoUserDetailsService implements UserDetailsService{

 

              @Autowired

              UserInfoRepository userRepo;

             

              @Override

              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

                             Optional<UserInfo> userInfo=userRepo.findByName(username);

                             return userInfo.map(UserInfoUserDetails::new).orElseThrow(() -> new UsernameNotFoundException("User not found: "+username));

              }

 

}

 

14. We create a separate endpoiny to store username and pwd in db, so in controller prg

 

   @PostMapping("/new")

              public String addNewUser(@RequestBody UserInfo userInfo) {

                             return movieService.addNewUser(userInfo);

              }

 

- In service program,

 

   @Autowired

              UserInfoRepository userRepo;

             

              @Autowired

              PasswordEncoder encoder;

             

              public String addNewUser(UserInfo userInfo) {

                          userInfo.setPassword(encoder.encode(userInfo.getPassword()));

                             userRepo.save(userInfo);

                             return "User added Successfully";

              }

 

15. We need to provide authorization for "/new" and also for swagger, so in SecurityConfig

 

private static final String[] WHITE_LIST_URL = { "/api/v1/auth/**", "/v2/api-docs", "/v3/api-docs",

            "/v3/api-docs/**", "/swagger-resources", "/swagger-resources/**", "/configuration/ui",

            "/configuration/security", "/swagger-ui/**", "/webjars/**", "/swagger-ui.html", "/api/auth/**",

            "/api/test/**", "/authenticate" };

 

 

@Bean

              public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{

                             return http.csrf(csrf->csrf.disable())

                                                             .authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new").permitAll()

                                                                                                   .requestMatchers(WHITE_LIST_URL).permitAll()

                                                                                                   .anyRequest().authenticated())

                                                             .formLogin().and().build();

              }

 

16. Start the appl, in swagger we add new user

 

{

  "id": 1000,

  "name": "Ram",

  "email": ram@gmail.com,

  "password": "abcd",

  "role": "ROLE_ADMIN"

}

{

  "id": 1001,

  "name": "Sam",

  "email": sam@gmail.com,

  "password": "xyz",

  "role": "ROLE_USER"

}

 

- Check database table

 

mysql> select * from user_info;

+------+---------------+------+--------------------------------------------------------------+------------+

| id   | email         | name | password                                                     | role       |

+------+---------------+------+--------------------------------------------------------------+------------+

| 1000 | ram@gmail.com | Ram  | $2a$10$YW0AeKjWujQzGefTwhjI6es0l65pEBothy344K5xihEMPT2Jdxh6a | ROLE_ADMIN |

| 1001 | sam@gmail.com | Sam  | $2a$10$yu4oxhXE.KmfON6h4QETbuCTYsveJp10FoJgmwCN0TV41uGnMzSw2 | ROLE_USER  |

+------+---------------+------+--------------------------------------------------------------+------------+

2 rows in set (0.00 sec)

 

17. We have defined UserDetailsService intf to communicate with db, we also need one more AuthenticationProvider bean to talk with UserDetailsService, so we have to configure DaoAuthenticationProvider in SecurityConfig

 

@Bean

              public AuthenticationProvider authenticationProvider() {

                             DaoAuthenticationProvider ap=new DaoAuthenticationProvider();

                             ap.setUserDetailsService(userDetailsService());

                             ap.setPasswordEncoder(passwordEncoder());

                             return ap;

              }

 

18. Start the appl

 

http://localhost:5000/api/welcome - everyone can access without username and pwd

http://localhost:5000/api/movie - only Ram can access with username and pwd

http://localhost:5000/api/movie/100 - only Sam can access  with username and pwd

 

 

JWT Authentication

    Previously in order to access each and every endpoint we have to provide username and pwd in login screen, instead what we want to implement is we allow the user to give credential for the first time,and we generate one token for that username and going forward the user can pass that token to access any endpoint in the appl, this token is called JWT(Json Web Token)

    JWT token has 3 parts

1. Header which contains algorithm which u want to generate the token and wht type of token

2. Payload contain claims which is nothing but subject(user), name for whom token is generated, when ur token is issued(iat), when ur token is expired(eat)

3. Verify Signature contains how we are generating the token

 

1. Add JWT dependency in pom.xml

 

       <dependency>

                                           <groupId>io.jsonwebtoken</groupId>

                                           <artifactId>jjwt-api</artifactId>

                                           <version>0.11.5</version>

                             </dependency>

                                           <dependency>

                                           <groupId>io.jsonwebtoken</groupId>

                                           <artifactId>jjwt-impl</artifactId>

                                           <version>0.11.5</version>

                             </dependency>

                             <dependency>

                                           <groupId>io.jsonwebtoken</groupId>

                                           <artifactId>jjwt-jackson</artifactId>

                                           <version>0.11.5</version>

                             </dependency>

 

2. Create DTO class called AuthRequest which contains username and pwd, based on this we are generating the token

 

@Data

@AllArgsConstructor

@NoArgsConstructor

public class AuthRequest {

   private String username;

   private String password;

}

 

3. Create endpoint in controller prg to generate the token based on username and pwd

 

@PostMapping("/authenticate")

              public String generateToken(@RequestBody AuthRequest authRequest) {

                            

              }

 

4. Now we have AuthRequest and we want to give the username to JWT to generate the token, so we create separate class called JwtService class

 

@Component

public class JwtService {

 

              public static final String SECRET="5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";

             

              //Generate the token based on username

              public String generateToken(String username) {

                             Map<String,Object> claims=new HashMap<>();

                             return createToken(claims, username);

              }

 

              //create JWT token

              private String createToken(Map<String, Object> claims, String username) {

                             return Jwts.builder()

                                                             .setClaims(claims)

                                                             .setSubject(username)

                                                             .setIssuedAt(new Date())

                                                             .setExpiration(new Date(System.currentTimeMillis()+1000*60*30))  //token is valid for 30min

                                                             .signWith(getSignInKey(), SignatureAlgorithm.HS256)

                                                             .compact();   

              }

 

              private Key getSignInKey() {

                             byte[] keyBytes=Decoders.BASE64.decode(SECRET);

                             return Keys.hmacShaKeyFor(keyBytes);

              }

}

 

5. Now in controller, we need to inject JwtService to generate the token

 

@Autowired

              JwtService jwtService;

             

              @PostMapping("/authenticate")

              public String generateToken(@RequestBody AuthRequest authRequest) {

                             return jwtService.generateToken(authRequest.getUsername());

              }

 

6. Now in SecurityConfig, we have to provide authorization for "/authenticate" endpoint

 

@Bean

              public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{

                             return http.csrf(csrf->csrf.disable())

                                                             .authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new","/api/authenticate").permitAll()

                                                                                                   .requestMatchers(WHITE_LIST_URL).permitAll()

                                                                                                   .anyRequest().authenticated())

                                                             .formLogin().and().build();

              }

 

7. Start the appl, in swagger we need to generate the token

{

  "username": "Ram",

  "password": "abcd"

}

It will generate the token for any users apart from user present in db or for invalid username and pwd

 

8. But we want to generate the token only for the users present inside db, so we need to add a restriction to the code so that only for users present inside db it has to generate the token

   Now we have to authenticate the user before generating the token

 

- In SecurityConfig, we have to inject a bean called AuthenticationManager

 

@Bean

              public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {

                             return config.getAuthenticationManager();

              }

 

- In "/authenticatee" endpoint, we have to authenticate user before generating token

 

@Autowired

              AuthenticationManager am;

             

              @PostMapping("/authenticate")

              public String generateToken(@RequestBody AuthRequest authRequest) {

                             Authentication auth=am.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));

                             if(auth.isAuthenticated())

                                  return jwtService.generateToken(authRequest.getUsername());

                             else

                                           throw new UsernameNotFoundException("Invalid user request");

              }

 

9. Start the appl,in swagger we need to generate the token

{

  "username": "Ram",

  "password": "abcd"

}

It will generate the token only for the correct users that are present in db

 

10. whenever client gives a request along with the token, we need to extract username from token and check with db whether that username exists or not, then only that user is validated so that we can access the endpoint

   So we create separate class called JwtAuthFilter that extends OncePerRequestFilter and override doFilterInternal() method

 

@Component

public class JwtAuthFilter extends OncePerRequestFilter {

 

              @Override

              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

                                           throws ServletException, IOException {

                             // TODO Auto-generated method stub

                            

              }

 

}

 

- When user send the request along with token, the token will be sending in header called "Authorization" as "Bearer token"

 

@Component

public class JwtAuthFilter extends OncePerRequestFilter {

             

              @Autowired

              JwtService jwtService;

             

              @Autowired

              UserInfoUserDetailsService userDetailsService;

 

              @Override

              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

                                           throws ServletException, IOException {

                            

                             //Extract token from Authorization header

                             String authHeader=request.getHeader("Authorization"); //Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJTYW0iLCJpYXQiOjE3NTk5OTM1OTUsImV4cCI6MTc1OTk5NTM5NX0.73Bj2GMFfUcbr9TycJtPVubaHI_QR45CUN0nkZ1BL6k

                            

                             String token=null;

                             String username=null;

                            

                             //check whether we have token or nor

                             if(authHeader != null && authHeader.startsWith("Bearer ")) {

                                           token=authHeader.substring(7); //eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJTYW0iLCJpYXQiOjE3NTk5OTM1OTUsImV4cCI6MTc1OTk5NTM5NX0.73Bj2GMFfUcbr9TycJtPVubaHI_QR45CUN0nkZ1BL6k

                                          

                                           //extract username from token

                                           username=jwtService.extractUsername(token);

                             }

                            

                             //Check username extracted from token is present in db or not

                             if(username !=null && SecurityContextHolder.getContext().getAuthentication()==null) {

                                           UserDetails userDetails=userDetailsService.loadUserByUsername(username);

                                          

                                           //validate JWT

                                           if(jwtService.validateToken(token,userDetails)) {

                                                          UsernamePasswordAuthenticationToken authToken=new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                                                          authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                                                         

                                                    SecurityContextHolder.getContext().setAuthentication(authToken);

                                           }

                             }

                             filterChain.doFilter(request, response);

              }

 

}

 

 

@Component

public class JwtService {

 

              public static final String SECRET="5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";

             

              //Generate the token based on username

              public String generateToken(String username) {

                             Map<String,Object> claims=new HashMap<>();

                             return createToken(claims, username);

              }

 

              //create JWT token

              private String createToken(Map<String, Object> claims, String username) {

                             return Jwts.builder()

                                                             .setClaims(claims)

                                                             .setSubject(username)

                                                             .setIssuedAt(new Date())

                                                             .setExpiration(new Date(System.currentTimeMillis()+1000*60*30))  //token is valid for 30min

                                                             .signWith(getSignInKey(), SignatureAlgorithm.HS256)

                                                             .compact();   

              }

 

              private Key getSignInKey() {

                             byte[] keyBytes=Decoders.BASE64.decode(SECRET);

                             return Keys.hmacShaKeyFor(keyBytes);

              }

 

              public String extractUsername(String token) {

                             return extractClaim(token,Claims::getSubject);

              }

 

              private <T> T extractClaim(String token, Function<Claims,T> claimResolver) {

                             final Claims claims=extractAllClaims(token);

                             return claimResolver.apply(claims);

              }

 

              private Claims extractAllClaims(String token) {

                             return Jwts.parserBuilder()

                                                             .setSigningKey(getSignInKey())

                                                             .build()

                                                             .parseClaimsJws(token)

                                                             .getBody();

              }

 

              public boolean validateToken(String token, UserDetails userDetails) {

                             final String username=extractUsername(token);

                             return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));

              }

 

              private boolean isTokenExpired(String token) {

                             return extractExpiration(token).before(new Date());

              }

 

              private Date extractExpiration(String token) {

                             return extractClaim(token,Claims::getExpiration);

              }

 

             

}

 

 

11. Now we tell SecurityConfig to use JwtAuthFilter which we have created before using any predefined filter, so in SecurityConfig we have to enable Session and authFilter

 

@Autowired

              JwtAuthFilter authFilter;

             

              @Bean

              public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{

                             return http.csrf(csrf->csrf.disable())

                                                             .authorizeHttpRequests(auth -> auth.requestMatchers("/api/welcome","/api/new","/api/authenticate").permitAll()

                                                                                                   .requestMatchers(WHITE_LIST_URL).permitAll()

                                                                                                   .anyRequest().authenticated())

                                                             .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                                                             .authenticationProvider(authenticationProvider())

                                                             .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)

                                                             .build();

              }

 

12. To make swagger to understand JWT token authentication and provide JWT token, we have to create separate class called SwaggerConfig

 

@Configuration

public class SwaggerConfig {

 

              @Bean

              public OpenAPI customOpenApi() {

                             return new OpenAPI().info(new Info().title("Movie Application with JWT Authentication"))

                                                            .addSecurityItem(new SecurityRequirement().addList("SecurityScheme"))

                                                            .components(new Components().addSecuritySchemes("SecurityScheme", new SecurityScheme().name("SecurityScheme").type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("jwt")));

              }

             

}

 

13. Start the appl

No comments:

Post a Comment