-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from 2024-pre-onboarding-backend-F/feat/login
[feat] 사용자 로그인 기능 구현
- Loading branch information
Showing
19 changed files
with
466 additions
and
50 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
60 changes: 29 additions & 31 deletions
60
src/main/java/wanted/media/content/repository/StatRepository.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 |
---|---|---|
@@ -1,43 +1,41 @@ | ||
package wanted.media.content.repository; | ||
|
||
import static com.querydsl.core.types.ExpressionUtils.*; | ||
import static wanted.media.content.domain.QPost.*; | ||
import static wanted.media.user.domain.QUser.*; | ||
|
||
import org.springframework.stereotype.Repository; | ||
|
||
import com.querydsl.jpa.impl.JPAQueryFactory; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Repository; | ||
import wanted.media.content.domain.dto.StatParam; | ||
|
||
import static com.querydsl.core.types.ExpressionUtils.count; | ||
import static wanted.media.content.domain.QPost.*; | ||
import static wanted.media.user.domain.QUser.user; | ||
|
||
@Repository | ||
@RequiredArgsConstructor | ||
public class StatRepository { | ||
private final JPAQueryFactory queryFactory; | ||
private final JPAQueryFactory queryFactory; | ||
|
||
/** | ||
* ex. | ||
* SELECT SUM(view_count) | ||
* FROM post p | ||
* LEFT JOIN members m ON p.user_id = m.user_id | ||
* where p.created_at between '2024-08-18 00:00:00' and '2024-08-25 23:59:59' | ||
* and m.account = 'user1'; | ||
*/ | ||
public Long statistics(StatParam param) { | ||
var selectQuery = switch (param.value()) { | ||
case COUNT -> queryFactory.select(count(post.id)); | ||
case LIKE_COUNT -> queryFactory.select(post.likeCount.sum()); | ||
case VIEW_COUNT -> queryFactory.select(post.viewCount.sum()); | ||
case SHARE_COUNT -> queryFactory.select(post.shareCount.sum()); | ||
}; | ||
/** | ||
* ex. | ||
* SELECT SUM(view_count) | ||
* FROM post p | ||
* LEFT JOIN members m ON p.user_id = m.user_id | ||
* where p.created_at between '2024-08-18 00:00:00' and '2024-08-25 23:59:59' | ||
* and m.account = 'user1'; | ||
*/ | ||
public Long statistics(StatParam param) { | ||
var selectQuery = switch (param.value()) { | ||
case COUNT -> queryFactory.select(count(post.id)); | ||
case LIKE_COUNT -> queryFactory.select(post.likeCount.sum()); | ||
case VIEW_COUNT -> queryFactory.select(post.viewCount.sum()); | ||
case SHARE_COUNT -> queryFactory.select(post.shareCount.sum()); | ||
}; | ||
|
||
return selectQuery.from(post) | ||
.leftJoin(user).on(post.user.userId.eq(user.userId)) | ||
.where( | ||
post.createdAt.between(param.start(), param.end()), | ||
post.user.account.eq(param.hashtag()) | ||
) | ||
.fetchFirst(); | ||
} | ||
return selectQuery.from(post) | ||
.leftJoin(user).on(post.user.userId.eq(user.userId)) | ||
.where( | ||
post.createdAt.between(param.start(), param.end()), | ||
post.user.account.eq(param.hashtag()) | ||
) | ||
.fetchFirst(); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,48 @@ | ||
package wanted.media.user.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.http.HttpStatus; | ||
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.http.SessionCreationPolicy; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
@EnableWebSecurity | ||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
private final TokenProvider tokenProvider; | ||
|
||
// 비밀번호 암호화 기능 | ||
@Bean | ||
public BCryptPasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
http | ||
.httpBasic(AbstractHttpConfigurer::disable) | ||
.csrf(AbstractHttpConfigurer::disable) | ||
.formLogin(AbstractHttpConfigurer::disable) | ||
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
.authorizeHttpRequests((auth) -> auth | ||
.requestMatchers("/user/**").permitAll() // 어떤 사용자든 접근 가능 | ||
.anyRequest().authenticated()) | ||
.exceptionHandling(e -> e | ||
.authenticationEntryPoint((request, response, exception) -> { | ||
response.sendError(HttpStatus.UNAUTHORIZED.value(), "인증이 필요합니다."); | ||
}) | ||
.accessDeniedHandler((request, response, exception) -> { | ||
response.sendError(HttpStatus.FORBIDDEN.value(), "접근권한이 없습니다."); | ||
})) | ||
.addFilterBefore(new TokenAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); | ||
|
||
return http.build(); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/wanted/media/user/config/TokenAuthenticationFilter.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,29 @@ | ||
package wanted.media.user.config; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
|
||
@RequiredArgsConstructor | ||
public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final TokenProvider tokenProvider; | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
String token = tokenProvider.resolveToken(request); // 헤더에서 가져온 액세스토큰 | ||
// 토큰 유효성 검증 | ||
if (token != null && tokenProvider.validToken(token)) { | ||
Authentication authentication = tokenProvider.getAuthentication(token); // 인증 정보 | ||
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 보안 컨텍스트에 설정 | ||
} | ||
filterChain.doFilter(request, response); // 응답과 요청을 다음 필터로 전달 | ||
} | ||
} |
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,83 @@ | ||
package wanted.media.user.config; | ||
|
||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Header; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.stereotype.Component; | ||
import wanted.media.user.domain.UserDetail; | ||
import wanted.media.user.service.UserDetailService; | ||
|
||
import java.util.Date; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class TokenProvider { | ||
|
||
@Value("${jwt.secret_key}") | ||
private String key; | ||
@Value("${jwt.access_token_expiration}") | ||
private long tokenValidTime; | ||
@Value("${jwt.refresh_token_expiration}") | ||
private long RefreshTokenValidTime; | ||
private final String BEARER_PREFIX = "Bearer "; | ||
|
||
private final UserDetailService userDetailService; | ||
|
||
public String makeToken(String account, String type) { | ||
Date now = new Date(); | ||
long time = type.equals("access") ? tokenValidTime : RefreshTokenValidTime; | ||
|
||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 | ||
.setClaims(Jwts.claims(). | ||
setSubject(account). | ||
setAudience(type). | ||
setIssuedAt(now). | ||
setExpiration(new Date(now.getTime() + time)) | ||
) | ||
.signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 | ||
.compact(); | ||
} | ||
|
||
// 토큰 유효성 검증 | ||
public boolean validToken(String token) { | ||
try { | ||
Jwts.parser().setSigningKey(key) | ||
.parseClaimsJws(token); | ||
return true; | ||
} catch (Exception e) { | ||
return false; | ||
} | ||
} | ||
|
||
// 토큰으로 인증 정보 담은 Authentication 반환 | ||
public Authentication getAuthentication(String token) { | ||
UserDetail userDetail = (UserDetail) userDetailService.loadUserByUsername(getUserAccount(token)); | ||
return new UsernamePasswordAuthenticationToken(userDetail, "", userDetail.getAuthorities()); | ||
/* principal : 인증된 사용자 정보 | ||
credentials : 사용자의 인증 자격 증명 (인증 완료된 상태이므로 빈 문자열 사용) | ||
authorities : 사용자의 권한목록*/ | ||
} | ||
|
||
public String getUserAccount(String token) { | ||
try { // JWT를 파싱해서 JWT 서명 검증 후 클레임을 반환하여 payload에서 subject 클레임 추출 | ||
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject(); | ||
} catch (ExpiredJwtException e) { | ||
return e.getClaims().getSubject(); | ||
} | ||
} | ||
|
||
// 토큰 Header에서 꺼내오기 | ||
public String resolveToken(HttpServletRequest request) { | ||
String header = request.getHeader("Authorization"); | ||
if (header != null && header.startsWith(BEARER_PREFIX)) | ||
return header.substring(BEARER_PREFIX.length()); | ||
return null; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/main/java/wanted/media/user/controller/TokenController.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,26 @@ | ||
package wanted.media.user.controller; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import wanted.media.user.dto.TokenRequestDto; | ||
import wanted.media.user.dto.TokenResponseDto; | ||
import wanted.media.user.service.TokenService; | ||
|
||
@RestController | ||
@RequestMapping("/api") | ||
@RequiredArgsConstructor | ||
public class TokenController { | ||
|
||
private final TokenService tokenService; | ||
|
||
@PostMapping("/token") | ||
public ResponseEntity<TokenResponseDto> getToken(@RequestBody TokenRequestDto requestDto) { | ||
TokenResponseDto responseDto = tokenService.getToken(requestDto); | ||
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); | ||
} | ||
} |
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
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,60 @@ | ||
package wanted.media.user.domain; | ||
|
||
import lombok.Builder; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
|
||
import java.util.Collection; | ||
import java.util.List; | ||
|
||
public class UserDetail implements UserDetails { | ||
private String account; | ||
private String password; | ||
private List<GrantedAuthority> authorities; | ||
|
||
@Builder | ||
public UserDetail(String account, String password, List<GrantedAuthority> authorities) { | ||
this.account = account; | ||
this.password = password; | ||
this.authorities = authorities; | ||
} | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
return authorities; | ||
} | ||
|
||
@Override | ||
public String getPassword() { | ||
return password; | ||
} | ||
|
||
@Override | ||
public String getUsername() { | ||
return account; | ||
} | ||
|
||
// 계정 만료 여부 | ||
@Override | ||
public boolean isAccountNonExpired() { | ||
return true; | ||
} | ||
|
||
// 계정 잠금 여부 | ||
@Override | ||
public boolean isAccountNonLocked() { | ||
return true; | ||
} | ||
|
||
// 비밀번호 만료 여부 | ||
@Override | ||
public boolean isCredentialsNonExpired() { | ||
return true; | ||
} | ||
|
||
// 계정 사용 가능 여부 | ||
@Override | ||
public boolean isEnabled() { | ||
return true; | ||
} | ||
} |
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,4 @@ | ||
package wanted.media.user.dto; | ||
|
||
public record TokenRequestDto(String accessToken, String refreshToken) { | ||
} |
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 wanted.media.user.dto; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@AllArgsConstructor | ||
public class TokenResponseDto { | ||
private String accessToken; | ||
private String refreshToken; | ||
} |
Oops, something went wrong.