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); } } }