-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* * #11 - feat: oauth, jwt, security test 의존성 삽입 * * #11 - feat: oauth, jwt 관련 설정 추가 OAuth provider 는 카카오와 네이버 * * #11 - feat: Member Entity에 email, name, oauth 관련 컬럼 추가 * * #11 - feat: Oauth2.0을 이용한 로그인 구현 Role 추가하지 않았고 세션 방식 없앴음 * * #11 - feat: OAuth 인증 후 JWT 발급 구현 refresh token, black list 추후 구현 * * #11 - feat: 기존 테스트 인증에도 잘 돌아가도록 수정 * * #11 - refactor: DTO 를 record로 변경 * * #11 - feat: 토큰 유효기간 변경 * * #11 - feat: 인증 중 유저 정보에 트랜잭션 처리 * * #11 - feat: 유저를 찾을 때 이메일이 아닌 OAuthID와 Provider로 유니크하게 찾게 변경 * * #11 - refactor: 불필요한 기본 생성자 삭제
- Loading branch information
Showing
21 changed files
with
674 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
src/main/java/cotato/bookitlist/config/security/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package cotato.bookitlist.config.security; | ||
|
||
import cotato.bookitlist.config.security.jwt.JwtAuthenticationFilter; | ||
import cotato.bookitlist.config.security.jwt.JwtTokenProvider; | ||
import cotato.bookitlist.config.security.oauth.service.CustomOAuth2UserService; | ||
import cotato.bookitlist.config.security.oauth.handler.OAuth2AuthenticationSuccessHandler; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.http.HttpMethod; | ||
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; | ||
import org.springframework.security.config.http.SessionCreationPolicy; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
@RequiredArgsConstructor | ||
@EnableWebSecurity | ||
@Configuration | ||
public class SecurityConfig { | ||
private final CustomOAuth2UserService customOAuth2UserService; | ||
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; | ||
private final JwtTokenProvider jwtTokenProvider; | ||
private final String[] WHITE_LIST = { | ||
"/health_check", | ||
"/swagger-ui/**", | ||
"/h2-console/**" | ||
}; | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
http | ||
.csrf(AbstractHttpConfigurer::disable) | ||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable).disable()) | ||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
.authorizeHttpRequests(auth -> auth | ||
.requestMatchers(WHITE_LIST).permitAll() | ||
.requestMatchers(HttpMethod.GET).permitAll() | ||
.anyRequest().authenticated() | ||
) | ||
.oauth2Login(oauth2 -> oauth2 | ||
.userInfoEndpoint((userInfo) -> userInfo.userService(this.customOAuth2UserService)) | ||
.successHandler(this.oAuth2AuthenticationSuccessHandler)) | ||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) | ||
|
||
.exceptionHandling(exception -> | ||
exception.authenticationEntryPoint((request, response, authException) -> | ||
response.sendError(HttpServletResponse.SC_FORBIDDEN))); | ||
return http.build(); | ||
} | ||
|
||
} |
54 changes: 54 additions & 0 deletions
54
src/main/java/cotato/bookitlist/config/security/jwt/AuthDetails.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package cotato.bookitlist.config.security.jwt; | ||
|
||
import java.util.Collection; | ||
import java.util.Collections; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
|
||
@AllArgsConstructor | ||
@Getter | ||
public class AuthDetails implements UserDetails { | ||
|
||
private String userId; | ||
|
||
private String role; | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role)); | ||
} | ||
|
||
@Override | ||
public String getPassword() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public String getUsername() { | ||
return userId; | ||
} | ||
|
||
@Override | ||
public boolean isAccountNonExpired() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isAccountNonLocked() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isCredentialsNonExpired() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean isEnabled() { | ||
return true; | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
src/main/java/cotato/bookitlist/config/security/jwt/JwtAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package cotato.bookitlist.config.security.jwt; | ||
|
||
import cotato.bookitlist.config.security.jwt.dto.AccessTokenInfo; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
|
||
import java.io.IOException; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
import org.springframework.web.util.WebUtils; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final JwtTokenProvider jwtTokenProvider; | ||
|
||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
String token = this.resolveToken(request); | ||
if (token != null) { | ||
Authentication authentication = this.getAuthentication(token); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
|
||
filterChain.doFilter(request, response); | ||
} | ||
|
||
private String resolveToken(HttpServletRequest request) { | ||
Cookie accessTokenCookie = WebUtils.getCookie(request, "accessToken"); | ||
if (accessTokenCookie != null) { | ||
return accessTokenCookie.getValue(); | ||
} else { | ||
String rawHeader = request.getHeader("Authorization"); | ||
String bearer = "Bearer "; | ||
return rawHeader != null && rawHeader.length() > bearer.length() && rawHeader.startsWith(bearer) ? rawHeader.substring(bearer.length()) : null; | ||
} | ||
} | ||
|
||
public Authentication getAuthentication(String token) { | ||
AccessTokenInfo accessTokenInfo = this.jwtTokenProvider.parseAccessToken(token); | ||
UserDetails userDetails = new AuthDetails(accessTokenInfo.userId().toString(), accessTokenInfo.role()); | ||
return new UsernamePasswordAuthenticationToken(userDetails, "user", userDetails.getAuthorities()); | ||
} | ||
|
||
} |
118 changes: 118 additions & 0 deletions
118
src/main/java/cotato/bookitlist/config/security/jwt/JwtTokenProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package cotato.bookitlist.config.security.jwt; | ||
|
||
import cotato.bookitlist.config.security.jwt.dto.AccessTokenInfo; | ||
import cotato.bookitlist.config.security.jwt.properties.JwtProperties; | ||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Jws; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.security.Key; | ||
import java.util.Date; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class JwtTokenProvider { | ||
|
||
private final JwtProperties jwtProperties; | ||
|
||
private Jws<Claims> getJws(String token) { | ||
try { | ||
return Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token); | ||
} catch (ExpiredJwtException ex) { | ||
throw new RuntimeException(); | ||
} catch (Exception ex) { | ||
throw new RuntimeException(); | ||
} | ||
} | ||
|
||
private Key getSecretKey() { | ||
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
private String buildAccessToken(Long id, Date issuedAt, Date accessTokenExpiresIn, String role) { | ||
final Key encodedKey = getSecretKey(); | ||
return Jwts.builder() | ||
.setIssuer("bookitlist") | ||
.setIssuedAt(issuedAt) | ||
.setSubject(id.toString()) | ||
.claim("type", "ACCESS_TOKEN") | ||
.claim("role", role) | ||
.setExpiration(accessTokenExpiresIn) | ||
.signWith(encodedKey) | ||
.compact(); | ||
} | ||
|
||
private String buildRefreshToken(Long id, Date issuedAt, Date accessTokenExpiresIn) { | ||
Key encodedKey = getSecretKey(); | ||
return Jwts.builder() | ||
.setIssuer("bookitlist") | ||
.setIssuedAt(issuedAt) | ||
.setSubject(id.toString()) | ||
.claim("type", "REFRESH_TOKEN") | ||
.setExpiration(accessTokenExpiresIn) | ||
.signWith(encodedKey) | ||
.compact(); | ||
} | ||
|
||
public String generateAccessToken(Long id, String role) { | ||
Date issuedAt = new Date(); | ||
Date accessTokenExpiresIn = new Date(issuedAt.getTime() + getAccessTokenTTlSecond() * 1000); | ||
|
||
return buildAccessToken(id, issuedAt, accessTokenExpiresIn, role); | ||
} | ||
|
||
public String generateRefreshToken(Long id) { | ||
Date issuedAt = new Date(); | ||
Date refreshTokenExpiresIn = new Date(issuedAt.getTime() + getRefreshTokenTTlSecond() * 1000); | ||
|
||
return buildRefreshToken(id, issuedAt, refreshTokenExpiresIn); | ||
} | ||
|
||
public boolean isAccessToken(String token) { | ||
return getJws(token).getBody().get("type").equals("ACCESS_TOKEN"); | ||
} | ||
|
||
public boolean isRefreshToken(String token) { | ||
return getJws(token).getBody().get("type").equals("REFRESH_TOKEN"); | ||
} | ||
|
||
public AccessTokenInfo parseAccessToken(String token) { | ||
if (isAccessToken(token)) { | ||
Claims claims = getJws(token).getBody(); | ||
return AccessTokenInfo.of( | ||
Long.parseLong(claims.getSubject()), | ||
(String) claims.get("role") | ||
); | ||
} else { | ||
throw new RuntimeException(); | ||
} | ||
} | ||
|
||
public Long parseRefreshToken(String token) { | ||
try { | ||
if (isRefreshToken(token)) { | ||
Claims claims = getJws(token).getBody(); | ||
return Long.parseLong(claims.getSubject()); | ||
} | ||
} catch (ExpiredJwtException ex) { | ||
throw new RuntimeException(); | ||
} | ||
|
||
throw new RuntimeException(); | ||
} | ||
|
||
public Long getRefreshTokenTTlSecond() { | ||
return jwtProperties.getRefreshExp(); | ||
} | ||
|
||
public Long getAccessTokenTTlSecond() { | ||
return jwtProperties.getAccessExp(); | ||
} | ||
|
||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/cotato/bookitlist/config/security/jwt/dto/AccessTokenInfo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package cotato.bookitlist.config.security.jwt.dto; | ||
|
||
public record AccessTokenInfo( | ||
Long userId, | ||
String role | ||
) { | ||
public static AccessTokenInfo of(Long userId, String role) { | ||
return new AccessTokenInfo(userId, role); | ||
} | ||
|
||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/cotato/bookitlist/config/security/jwt/properties/JwtProperties.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package cotato.bookitlist.config.security.jwt.properties; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@ConfigurationProperties(prefix = "auth.jwt") | ||
public class JwtProperties { | ||
private String secretKey; | ||
private Long accessExp; | ||
private Long refreshExp; | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/cotato/bookitlist/config/security/oauth/AuthProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package cotato.bookitlist.config.security.oauth; | ||
|
||
public enum AuthProvider { | ||
KAKAO, | ||
NAVER, | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/cotato/bookitlist/config/security/oauth/KakaoOAuth2User.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package cotato.bookitlist.config.security.oauth; | ||
|
||
import java.util.Map; | ||
|
||
public class KakaoOAuth2User extends OAuth2UserInfo { | ||
private final Long id; | ||
|
||
public KakaoOAuth2User(Map<String, Object> attributes) { | ||
super((Map) attributes.get("kakao_account")); | ||
this.id = (Long) attributes.get("id"); | ||
} | ||
|
||
@Override | ||
public String getOAuth2Id() { | ||
return this.id.toString(); | ||
} | ||
|
||
@Override | ||
public String getEmail() { | ||
return (String) this.attributes.get("email"); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return (String) ((Map) this.attributes.get("profile")).get("nickname"); | ||
} | ||
} |
Oops, something went wrong.