Skip to content

Commit

Permalink
feat: jwt 구현
Browse files Browse the repository at this point in the history
- 로그인 시 jwt 발급 api 구현
- swagger 토큰 기능 추가
- security jwt 설정
  • Loading branch information
SJ70 committed Jun 30, 2024
1 parent 3e373be commit 6c0d296
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 5 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/com/j9/bestmoments/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.j9.bestmoments.auth;

import com.j9.bestmoments.auth.jwt.JwtToken;
import com.j9.bestmoments.auth.jwt.JwtTokenProvider;
import com.j9.bestmoments.auth.oauth.dto.request.OAuthUserInfoDto;
import com.j9.bestmoments.auth.oauth.service.GoogleAuthService;
import com.j9.bestmoments.auth.oauth.service.OAuthService;
import com.j9.bestmoments.member.Member;
import com.j9.bestmoments.member.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -20,17 +25,21 @@
public class AuthController {

private final GoogleAuthService googleAuthService;
private final MemberService memberService;
private final JwtTokenProvider jwtTokenProvider;

@GetMapping("/login/{oAuthProvider}")
@Operation(summary = "OAuth 인증코드로 로그인/회원가입", description = "oAuthProvider: google")
public String login(@PathVariable String oAuthProvider, @RequestParam String code) {
public ResponseEntity<JwtToken> login(@PathVariable String oAuthProvider, @RequestParam String code) {
OAuthService oAuthService = switch (oAuthProvider) {
case "google" -> googleAuthService;
default -> throw new OAuth2AuthenticationException("존재하지 않는 OAuth 인증 방식입니다.");
};
OAuthUserInfoDto oAuthUserInfo = oAuthService.getUserInfo(code);
log.info(oAuthUserInfo.toString());
return "토큰 발급";
Member member = memberService.findOrSaveByOAuthInfo(oAuthUserInfo);

JwtToken jwtToken = jwtTokenProvider.generateToken(member);
return ResponseEntity.ok(jwtToken);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.j9.bestmoments.auth.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

private final JwtTokenProvider jwtTokenProvider;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = resolveToken((HttpServletRequest) request);

if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7);
}
return null;
}


}
9 changes: 9 additions & 0 deletions src/main/java/com/j9/bestmoments/auth/jwt/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.j9.bestmoments.auth.jwt;

public record JwtToken(
String grantType,
String accessToken,
String refreshToken
) {

}
106 changes: 106 additions & 0 deletions src/main/java/com/j9/bestmoments/auth/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.j9.bestmoments.auth.jwt;

import com.j9.bestmoments.member.Member;
import com.sun.security.auth.UserPrincipal;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Collections;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class JwtTokenProvider {

private final Key key;

@Value("${jwt.accessTokenExpirationMs}")
private long accessTokenExpirationMs;

@Value("${jwt.refreshTokenExpirationMs}")
private long refreshTokenExpirationMs;

// secret 값을 암호화 (SHA 키 생성)
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public JwtToken generateToken(Member member) {

Date now = new Date();
Date accessTokenExpiresIn = new Date(now.getTime() + accessTokenExpirationMs);
Date refreshTokenExpiresIn = new Date(now.getTime() + refreshTokenExpirationMs);

// 액세스 토큰 생성
String accessToken = Jwts.builder()
.claim("id", member.getId())
.claim("role", member.getRole())
.setIssuedAt(now)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

// 리프레쉬 토큰 생성
String refreshToken = Jwts.builder()
.claim("id", member.getId())
.setIssuedAt(now)
.setExpiration(refreshTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

return new JwtToken("Bearer", accessToken, refreshToken);
}

// 토큰을 복호화하여 인증 정보 추출
public Authentication getAuthentication(String accessToken) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();

if (claims.get("id") == null || claims.get("role") == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}

String id = claims.get("id").toString();
String role = claims.get("role").toString();

SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role);
return new UsernamePasswordAuthenticationToken(new UserPrincipal(id), "", Collections.singletonList(authority));
}

// 토큰 정보 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
}
return false;
}

}
26 changes: 24 additions & 2 deletions src/main/java/com/j9/bestmoments/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
package com.j9.bestmoments.config;

import com.j9.bestmoments.auth.jwt.JwtAuthenticationFilter;
import com.j9.bestmoments.auth.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable).disable())
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().permitAll()
// .anyRequest().authenticated()
)

.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);


.authorizeHttpRequests(auth ->
auth.anyRequest().permitAll());
;

return http.build();
}
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/j9/bestmoments/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@OpenAPIDefinition(
Expand All @@ -14,4 +19,23 @@
@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI(){

Components components = new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
);

SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth");

return new OpenAPI()
.components(components)
.addSecurityItem(securityRequirement);
}

}

0 comments on commit 6c0d296

Please sign in to comment.