diff --git a/build.gradle b/build.gradle index 4e1e2a9..4620bb6 100644 --- a/build.gradle +++ b/build.gradle @@ -49,13 +49,17 @@ dependencies { /* Security */ -// implementation 'org.springframework.boot:spring-boot-starter-security:2.6.7' implementation 'org.springframework.boot:spring-boot-starter-security:2.7.0' /* Validation */ implementation 'org.springframework.boot:spring-boot-starter-validation' + + /* + Jwt (JSON Web Token Support For The JVM) + */ + implementation 'io.jsonwebtoken:jjwt:0.9.1' } tasks.named('test') { diff --git a/src/main/java/demo/api/config/SecurityConfig.java b/src/main/java/demo/api/config/SecurityConfig.java index 814dc64..6ec14a2 100644 --- a/src/main/java/demo/api/config/SecurityConfig.java +++ b/src/main/java/demo/api/config/SecurityConfig.java @@ -40,7 +40,7 @@ public class SecurityConfig { .authorizeRequests() .antMatchers("/", "/user/signUp", "/user/userList", "/user/signIn*").permitAll() .anyRequest().authenticated(); - + return http.build(); } } diff --git a/src/main/java/demo/api/jwt/JwtTokenFilter.java b/src/main/java/demo/api/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..b319159 --- /dev/null +++ b/src/main/java/demo/api/jwt/JwtTokenFilter.java @@ -0,0 +1,42 @@ +package demo.api.jwt; + +import demo.api.jwt.exception.CustomException; +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.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +// Request 이전에 1회 작동할 필터 +public class JwtTokenFilter extends OncePerRequestFilter { + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(request); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (CustomException ex) { + //this is very important, since it guarantees the user is not authenticated at all + SecurityContextHolder.clearContext(); + response.sendError(ex.getHttpStatus().value(), ex.getMessage()); + return; + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/demo/api/jwt/JwtTokenProvider.java b/src/main/java/demo/api/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..590d064 --- /dev/null +++ b/src/main/java/demo/api/jwt/JwtTokenProvider.java @@ -0,0 +1,84 @@ +package demo.api.jwt; + +import demo.api.jwt.exception.CustomException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import java.util.Base64; +import java.util.Date; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; + +// 유저 정보로 JWT 토큰을 만들거나 토큰을 바탕으로 유저 정보를 가져옴 +@Component +public class JwtTokenProvider { + /** + * THIS IS NOT A SECURE PRACTICE! For simplicity, we are storing a static key here. Ideally, in a + * microservices environment, this key would be kept on a config-server. + */ + @Value("${jwt.token.open-secret-key}") + private String secret_key; + + @Value("${jwt.token.expire-length}") + private long expire_time; + + @Autowired + private UserDetailsService userDetailsService; + + @PostConstruct // 의존성 주입이 이루어진 후 초기화를 수행 + protected void init() { + secret_key = Base64.getEncoder().encodeToString(secret_key.getBytes()); + } + + public String createToken(String username, List appUserRoles) { + + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", appUserRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + + Date now = new Date(); + Date validity = new Date(now.getTime() + expire_time); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secret_key) + .compact(); + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token).getBody().getSubject(); + } + + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secret_key).parseClaimsJws(token); + return true; + } catch (JwtException e) { + // MalformedJwtException | ExpiredJwtException | IllegalArgumentException + throw new CustomException("Error on Token", HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/demo/api/jwt/exception/CustomException.java b/src/main/java/demo/api/jwt/exception/CustomException.java new file mode 100644 index 0000000..9aa49d6 --- /dev/null +++ b/src/main/java/demo/api/jwt/exception/CustomException.java @@ -0,0 +1,26 @@ +package demo.api.jwt.exception; + +import org.springframework.http.HttpStatus; + +public class CustomException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String message; + private final HttpStatus httpStatus; + + public CustomException(String message, HttpStatus httpStatus) { + this.message = message; + this.httpStatus = httpStatus; + } + + @Override + public String getMessage() { + return message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 118e996..49c5873 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,4 +8,8 @@ spring: jpa: show-sql: true hibernate: - ddl-auto: none \ No newline at end of file + ddl-auto: none + jwt: + token: + secret-key: open-secret-key + expire-length: 300000 \ No newline at end of file