Spring + RESTful + JWT

RFC7519 : JWT

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- 追加 -->
<dependency>
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
</dependency> 
<dependency>
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency>
<dependency> 
    <groupId>io.jsonwebtoken</groupId> 
    <artifactId>jjwt</artifactId> 
    <version>0.9.1</version> 
</dependency>
<dependency> 
    <groupId>javax.xml.bind</groupId> 
    <artifactId>jaxb-api</artifactId> 
</dependency> 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test-autoconfigure</artifactId>
    <scope>test</scope>
</dependency>

application.properties

secret=somerandomsecret

main

package jp.kd2.jwttest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JwttestApplication {

    public static void main(String[] args) {
        SpringApplication.run(JwttestApplication.class, args);
    }

}

RequestModel

package jp.kd2.jwttest.model;

import java.io.Serializable;

public class JwtRequest implements Serializable {
    
    private static final long serialVersionUID = 1L; 
   
       private String username; 
   
       private String password; 
   
       public JwtRequest() { 
       } 
   
       public JwtRequest(String username, String password) { 
           super(); 
           this.username = username; this.password = password; 
       } 
   
       public String getUsername() { 
           return username;
       } 
   
       public void setUsername(String username) { 
           this.username = username; 
       } 
   
       public String getPassword() { 
           return password; 
       } 
   
       public void setPassword(String password) {
           this.password = password; 
       }
}

ResponseModel

package jp.kd2.jwttest.model;

import java.io.Serializable;

public class JwtResponse implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private final String token;
    
    public JwtResponse(String token) {
        this.token = token;
    }
    
    public String getToken() {
        return token;
    }
}

RestController

package jp.kd2.jwttest.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController 
public class HelloWorldController {

    @GetMapping("/hello") 
    public String hello() { 
        return "{ \"greeting\" : \"Hello World Rest\" }"; 
    } 
}

JWTController

package jp.kd2.jwttest.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
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.RestController;
import jp.kd2.jwttest.service.JwtUserDetailsService;
import jp.kd2.jwttest.model.JwtRequest;
import jp.kd2.jwttest.model.JwtResponse;
import jp.kd2.jwttest.util.JwtTokenManager;

@RestController
@CrossOrigin
public class JwtController {

    @Autowired
    private JwtUserDetailsService userDetailsService;
   
    @Autowired
    private AuthenticationManager authenticationManager;
   
    @Autowired
    private JwtTokenManager tokenManager;
   
    @PostMapping("/login")
    public ResponseEntity<?> createToken(
            @RequestBody JwtRequest request) throws Exception {
     
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            request.getUsername(), request.getPassword()
                    )
            );
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
        
        final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        final String jwtToken = tokenManager.generateJwtToken(userDetails);
        
        return ResponseEntity.ok(new JwtResponse(jwtToken));
   }
}

SecurityConfig

package jp.kd2.jwttest.config;

import org.springframework.beans.factory.annotation.Autowired;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import jp.kd2.jwttest.util.JwtAuthenticationEntryPoint;
import jp.kd2.jwttest.util.JwtFilter; 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSecurityConfig {
    
   @Autowired
   private JwtAuthenticationEntryPoint authenticationEntryPoint;
      
   @Autowired
   private JwtFilter filter;
      
   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
   
   @Bean
   public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
       return authenticationConfiguration.getAuthenticationManager();
   }
   
   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
             
       http.cors().and().csrf().disable()
       .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
       .authorizeRequests()
       .antMatchers("/login").permitAll()
       .anyRequest().authenticated();
       
       http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);

       return http.build();
   }
   
   @Bean
   public WebMvcConfigurer corsConfigurer() {
       return new WebMvcConfigurer() {
           @Override
           public void addCorsMappings(CorsRegistry registry) {
               registry.addMapping("/**").allowedMethods("*");
           }
       };
   }   
}

Service

package jp.kd2.jwttest.service;

import java.util.ArrayList; 
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; 

@Service
public class JwtUserDetailsService implements UserDetailsService { 
    
    @Override 
    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
       
       if ("jwtuser".equals(username)) { 
           return new User("jwtuser", 
                   "$2a$10$Ik4eh/jP3VBumhbcCfonseroGTrL1brrSBirm3aOXWJB09N9734/i",
                   new ArrayList<>()); 
       } else { 
           throw new UsernameNotFoundException("User was not found : " + username); 
       } 
    } 
}

EntryPoint

package jp.kd2.jwttest.util;

import java.io.IOException;
import java.io.Serializable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class JwtAuthenticationEntryPoint
    implements AuthenticationEntryPoint, Serializable {
    
    @Override
    public void commence(
            HttpServletRequest request, 
            HttpServletResponse response, 
            AuthenticationException authException)
                    throws IOException, ServletException {
        
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
  }
}

Filter

package jp.kd2.jwttest.util;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;

import jp.kd2.jwttest.service.JwtUserDetailsService;

@Component
public class JwtFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUserDetailsService userDetailsService;
   
    @Autowired
    private JwtTokenManager tokenManager;
   
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response, 
            FilterChain filterChain)
                throws ServletException, IOException {
      
        String tokenHeader = request.getHeader("Authorization");
        String username = null;
        String token = null;
        if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
            token = tokenHeader.substring(7);
            
            try {
                username = tokenManager.getUsernameFromToken(token);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else {
            System.out.println("Bearer String was not found in token");
        }
        
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (tokenManager.validateJwtToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken = 
                        new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

TokenManager

package jp.kd2.jwttest.util;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; 

@Component 
public class JwtTokenManager implements Serializable {

    private static final long serialVersionUID = 1L; 
    
    public static final long TOKEN_VALIDITY = 600; // 600sec = 10mins
    
    @Value("${secret}") 
    private String jwtSecret; 
   
    public String generateJwtToken(UserDetails userDetails) { 
        Map<String, Object> claims = new HashMap<>(); 
        
        return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, jwtSecret).compact(); 
    }
    
    public Boolean validateJwtToken(String token, UserDetails userDetails) { 
        String username = getUsernameFromToken(token); 
        Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
        Boolean isTokenExpired = claims.getExpiration().before(new Date()); 
        return (username.equals(userDetails.getUsername()) && !isTokenExpired); 
    }
    
    public String getUsernameFromToken(String token) {
        final Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody(); 
        return claims.getSubject(); 
    } 
}

JUnitでテストするクラス。

package jp.kd2.jwttest;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
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.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

/**
 * テストクラス
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class JwtAuthenticationTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testJwtAuthentication() throws Exception {
        
        // 正しいユーザ/パスワード : 200が返る。
        this.mockMvc.perform(post("/login")
            .content("{ \"username\":\"jwtuser\", \"password\":\"password\" }")
            .contentType("application/json"))
            .andDo(print()).andExpect(status().isOk());
        // 間違ったパスワード : 401が返る。
        this.mockMvc.perform(post("/login")
            .content("{ \"username\":\"jwtuser\", \"password\":\"wrongpassword\" }")
            .contentType("application/json"))
            .andDo(print()).andExpect(status().is4xxClientError());
    }
}

パスワードを確認するクラス。

package jp.kd2.jwttest;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PasswordTest {

    public static void main(String[] args) {
        // 同じパスワードでも毎回違う値が返ることを確認する。
        for (int i = 0; i < 100; i++) {
            String password = "password";
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            String encrypted = passwordEncoder.encode(password);
            System.out.println(encrypted);    
        }
    }
}

 

投稿日: