WEB/Security

[java brains] JWT+ Spring Security

Tony Lim 2021. 4. 18. 00:32
728x90
@Service
public class JwtUtil
{
    private String SECRET_KEY = "secret";

    public String extractUsername(String token)
    {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token)
    {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token , Function<Claims,T> claimsResolver)
    {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token)
    {
        return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token)
    {
        return extractExpiration(token).before(new Date());
    }
    public String generateToken(UserDetails userDetails)
    {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims,userDetails.getUsername());
    }

    private String createToken(Map<String,Object> claims, String subject)
    {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 *10))
                .signWith(SignatureAlgorithm.HS256,SECRET_KEY).compact();
    }

    public Boolean validateToken(String token,UserDetails userDetails)
    {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

}

generateToken method create "claims" for payload and sends to "createToken" method with username

createToken method set bunch of infomration and the "SIGN" wtih Secret Key. there are other lots of api for building JWT token.

extractClaim let us extract extractUsername , extractExpiration 

The actual validation happens in the "extract Claims" method, and if the parsing goes wrong, an exception is raised, so the "validateToken" method is not required.

 

@Getter
@RequiredArgsConstructor
public class AuthenticationResponse
{
    private final String jwt;
}
@Getter @Setter @NoArgsConstructor(access = AccessLevel.PUBLIC)
public class AuthenticationRequest
{
    private String username;
    private String password;

    public AuthenticationRequest(String username, String password)
    {
        this.username = username;
        this.password = password;
    }
}
    @PostMapping( "/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception
    {
        try
        {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        } catch (BadCredentialsException e)
        {
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = myUserDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }

 api endpoint where we authenticate given request. 

we just give AuthenticationManager how we are going to authenticate (by giving authenticate instance with property  and AuthenticationMangaer will look for proper AuthenticationProvider , who acutually does auththenticate, and return authenticated authenticate instance.

 

after authentication success we return JWT

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
    private final DataSource dataSource;
    private final MyUserDetailsService myUserDetailsService;
    private final JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
       auth.userDetailsService(myUserDetailsService).passwordEncoder(getPasswordEncoder());
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers("/user").hasAnyRole("ADMIN","USER")
//                .antMatchers("/").permitAll()
                .antMatchers("/authenticate").permitAll().anyRequest().authenticated()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

    }

    @Bean
    public PasswordEncoder getPasswordEncoder()
    {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }
}

when requesting for ("/authenticate") it is always authenticated and verify username and password. 

after having jwt every request will be validate with {"Auhthentication" : "JWT"} and Roles

@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter
{
    private final MyUserDetailsService myUserDetailsService;
    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
    {

        String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer "))
        {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null)
        {
            UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
            if(jwtUtil.validateToken(jwt,userDetails))
            {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }
}

this filter will verify token before UsernameandPasswordFilter.

@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService
{
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        User user = userRepository.findByUserName(username).
                orElseThrow(() -> new UsernameNotFoundException("Not found: +" + username));

        MyUserDetails myUserDetails = new MyUserDetails(user);

        return myUserDetails;
    }
}

for user information we used UserDetailService which is dependent with JPArepository

728x90

'WEB > Security' 카테고리의 다른 글

OAuth 2.0 and OpenID Connect (OAtuh 2.0 in depth)  (0) 2021.12.19
[java brains] OAuth2 , Google  (0) 2021.04.26
[java brains] JWT  (0) 2021.04.15
[java brains] JPA authentication  (0) 2021.04.14
[java brains] how Spring Security Authentication works  (0) 2021.04.12