-
Notifications
You must be signed in to change notification settings - Fork 0
사용자 회원가입 및 로그인 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f55b98
30674da
aa833ba
4e9eac1
b5d6bab
571071e
1eeb0b3
b453612
95b0752
3485dc7
65ce793
a681ba8
0f63580
378e238
dd5f1c3
0c0e967
62a4eb0
734a376
5888963
737ca66
55927ec
f9ffcb1
b3c73ad
585ab89
aefbcaf
238dc33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,33 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| app: | ||
| build: . | ||
| ports: | ||
| - "8080:8080" | ||
| environment: | ||
| SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb | ||
| SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} | ||
| SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} | ||
| depends_on: | ||
| - db | ||
| environment: | ||
| - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/${MYSQL_DATABASE} | ||
| - SPRING_DATASOURCE_USERNAME=${MYSQL_USER} | ||
| - SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD} | ||
| - JWT_SECRET=${JWT_SECRET} | ||
| - JWT_EXPIRATION=${JWT_EXPIRATION} | ||
| volumes: | ||
| - .:/workspace | ||
| working_dir: /workspace | ||
|
|
||
| db: | ||
| image: mysql:8.0 | ||
| environment: | ||
| MYSQL_DATABASE: mydb | ||
| MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} | ||
| MYSQL_DATABASE: ${MYSQL_DATABASE} | ||
| MYSQL_USER: ${MYSQL_USER} | ||
| MYSQL_PASSWORD: ${MYSQL_PASSWORD} | ||
| MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} | ||
| ports: | ||
| - "3306:3306" | ||
| - "3306:3306" | ||
| volumes: | ||
| - mysql_data:/var/lib/mysql | ||
|
|
||
| volumes: | ||
| mysql_data: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.example.tech_interview_buddy.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.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jwt를 쓰기로 한 이유가 있을까요 ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 솔직히 말씀드리면 토큰이 필요하다는 생각이 들었을 때 가장 먼저 떠오른게 JWT여서-이긴 했는데요, |
||
|
|
||
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final UserDetailsService userDetailsService; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, | ||
| HttpServletResponse response, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스타일가이드가 없는 것 같은데요, 스타일가이드 적용 예제 보고 한번 따라해보시면 좋을 것 같아요 ~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정하겠습니다 |
||
| FilterChain filterChain) throws ServletException, IOException { | ||
|
|
||
| if (request.getRequestURI().equals("/api/v1/users/login")) { | ||
| filterChain.doFilter(request, response); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| String jwt = getJwtFromRequest(request); | ||
|
|
||
| if (jwt != null && jwtTokenProvider.validateToken(jwt)) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null체크같은 경우는 조건이 맞지 않는 경우 얼리리턴을 해주는게 가독성에 도움이 될 것 같아요 ~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정하겠습니다 |
||
| String username = jwtTokenProvider.getUsernameFromToken(jwt); | ||
| UserDetails userDetails = userDetailsService.loadUserByUsername(username); | ||
|
|
||
| UsernamePasswordAuthenticationToken authentication = | ||
| new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); | ||
| authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| } | ||
| } catch (Exception ex) { | ||
| logger.error("Could not set user authentication in security context", ex); | ||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private String getJwtFromRequest(HttpServletRequest request) { | ||
| String bearerToken = request.getHeader("Authorization"); | ||
| if (bearerToken != null && bearerToken.startsWith("Bearer ")) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런건 상수로 정의해보면 좋을 것 같아요 ~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정하겠습니다 |
||
| return bearerToken.substring(7); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.example.tech_interview_buddy.config; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.JwtException; | ||
| import io.jsonwebtoken.Jwts; | ||
| import io.jsonwebtoken.SignatureAlgorithm; | ||
| import io.jsonwebtoken.security.Keys; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import javax.crypto.SecretKey; | ||
| import java.util.Date; | ||
|
|
||
| @Component | ||
| public class JwtTokenProvider { | ||
|
|
||
| @Value("${jwt.secret:defaultSecretKey}") | ||
| private String secretKey; | ||
|
|
||
| @Value("${jwt.expiration:86400000}") | ||
| private long validityInMilliseconds; | ||
|
|
||
| private SecretKey getSigningKey() { | ||
| return Keys.hmacShaKeyFor(secretKey.getBytes()); | ||
| } | ||
|
|
||
| public String generateToken(Authentication authentication) { | ||
| UserDetails userDetails = (UserDetails) authentication.getPrincipal(); | ||
| Date now = new Date(); | ||
| Date expiryDate = new Date(now.getTime() + validityInMilliseconds); | ||
|
|
||
| return Jwts.builder() | ||
| .setSubject(userDetails.getUsername()) | ||
| .setIssuedAt(now) | ||
| .setExpiration(expiryDate) | ||
| .signWith(getSigningKey(), SignatureAlgorithm.HS512) | ||
| .compact(); | ||
| } | ||
|
|
||
| public String getUsernameFromToken(String token) { | ||
| Claims claims = Jwts.parserBuilder() | ||
| .setSigningKey(getSigningKey()) | ||
| .build() | ||
| .parseClaimsJws(token) | ||
| .getBody(); | ||
|
|
||
| return claims.getSubject(); | ||
| } | ||
|
|
||
| public boolean validateToken(String token) { | ||
| try { | ||
| Jwts.parserBuilder() | ||
| .setSigningKey(getSigningKey()) | ||
| .build() | ||
| .parseClaimsJws(token); | ||
| return true; | ||
| } catch (JwtException | IllegalArgumentException e) { | ||
| return false; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.example.tech_interview_buddy.config; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| 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.web.builders.HttpSecurity; | ||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
| import org.springframework.security.config.http.SessionCreationPolicy; | ||
| import org.springframework.security.core.userdetails.UserDetailsService; | ||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
| import org.springframework.security.web.SecurityFilterChain; | ||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
|
||
| @Configuration | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 어플리케이션 수행 해 보셨나요 ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 직접 포스트맨으로 회원가입, 로그인 동작하는 것 확인하고 DB에도 사용자 레코드 확인했습니다 |
||
| @EnableWebSecurity | ||
| @RequiredArgsConstructor | ||
| public class SecurityConfig { | ||
|
|
||
| private final JwtAuthenticationFilter jwtAuthenticationFilter; | ||
| private final UserDetailsService userDetailsService; | ||
|
|
||
| @Bean | ||
| public BCryptPasswordEncoder bCryptPasswordEncoder() { | ||
| return new BCryptPasswordEncoder(); | ||
| } | ||
|
|
||
| @Bean | ||
| public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { | ||
| return config.getAuthenticationManager(); | ||
| } | ||
|
|
||
| @Bean | ||
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
| http | ||
| .csrf(csrf -> csrf.disable()) | ||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
| .authorizeHttpRequests(authz -> authz | ||
| .requestMatchers("/api/v1/users/**").permitAll() | ||
| .anyRequest().authenticated() | ||
| ) | ||
| .userDetailsService(userDetailsService) | ||
| .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||
|
|
||
| return http.build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.example.tech_interview_buddy.controller; | ||
|
|
||
| import com.example.tech_interview_buddy.config.JwtTokenProvider; | ||
| import com.example.tech_interview_buddy.service.UserService; | ||
| import com.example.tech_interview_buddy.dto.request.UserCreateRequest; | ||
| import com.example.tech_interview_buddy.dto.request.UserLoginRequest; | ||
| import com.example.tech_interview_buddy.dto.response.UserLoginResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.security.authentication.AuthenticationManager; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import jakarta.validation.Valid; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/users") | ||
| @RequiredArgsConstructor | ||
| public class UserController { | ||
|
|
||
| private final UserService userService; | ||
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final AuthenticationManager authenticationManager; | ||
|
|
||
| @GetMapping | ||
| public String getUsers() { | ||
| return "users"; | ||
| } | ||
|
|
||
| @PostMapping("") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빈 문자열은 굳이 필요 없어보여요 ~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정하겠습니다 |
||
| @ResponseStatus(HttpStatus.CREATED) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 좀 생각해 볼 필요가 있는데요, 클라이언트 사이드에서 성공 응답을
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 성공 응답 세분화까지는 안하는 방향으로 수정하겠습니다 |
||
| public ResponseEntity<Void> registerUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { | ||
| userService.save(userCreateRequest); | ||
| return ResponseEntity.status(HttpStatus.CREATED).build(); | ||
| } | ||
|
|
||
| @PostMapping("/login") | ||
| public ResponseEntity<UserLoginResponse> loginUser(@Valid @RequestBody UserLoginRequest userLoginRequest) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요거는 몇번 말씀드렸는데, 그냥 응답에 성공하면 기본적으로 200 응답이 내려가기때문에 굳이 응답을 감싸진 않아도 될 것 같아요
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드를 쓰고 지우고 반복하는 과정에서 또 되살아난 것 같습니다. 주의하겠습니다! |
||
| Authentication authentication = authenticationManager.authenticate( | ||
| new UsernamePasswordAuthenticationToken(userLoginRequest.getUsername(), userLoginRequest.getPassword()) | ||
| ); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| String jwt = jwtTokenProvider.generateToken(authentication); | ||
|
|
||
| return ResponseEntity.ok(new UserLoginResponse(jwt)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.example.tech_interview_buddy.domain; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Builder; | ||
|
|
||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @Entity | ||
| @Table(name = "user", uniqueConstraints = { | ||
| @UniqueConstraint(columnNames = "username"), | ||
| @UniqueConstraint(columnNames = "email") | ||
| }) | ||
| public class User { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false) | ||
| private String username; | ||
|
|
||
| @Column(nullable = false) | ||
| private String password; | ||
|
|
||
| @Column(nullable = false) | ||
| private String email; | ||
|
|
||
| @Builder | ||
| public User(String username, String password, String email) { | ||
| this.username = username; | ||
| this.password = password; | ||
| this.email = email; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.example.tech_interview_buddy.dto.request; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.AllArgsConstructor; | ||
|
|
||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class UserCreateRequest { | ||
| @NotBlank(message = "Username is required") | ||
| private String username; | ||
|
|
||
| @NotBlank(message = "Email is required") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유효성검사를 여기서 하는건 좀 생각해볼 필요가 있는데요, 설정이 쉽고 요청단에서 어떤 값을 가지는지 의미를 명확히 할 수 있다는 장점도 있지만, 코드 침투가 일어나서 가독성이 떨어지는 것과, 어차피 서비스단에서 유효성검사를 하기 때문에 중복으로 검사를 하게 되고 관리 포인트가 늘어나는 단점이 있어요. 컨트롤러 - 서비스 - 도메인 중 유효성검사는 어디서 해야하나 라는걸로 의견이 갈릴때가 많은데, 굳이 따지자면 저는 도메인에 가까운 순으로 하는게 맞다고 생각하고, 제일 좋은건 각 레이어에서 각각 하는게 제일 좋긴 하다고 생각합니다
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컨트롤러, 서비스, 도메인 3개의 레이어에서 각각 유효성검사를 수행하되, dto에서까지는 수행하지 않는 것으로 이해했습니다. |
||
| @Email(message = "Invalid email address") | ||
| private String email; | ||
|
|
||
| @NotBlank(message = "Password is required") | ||
| @Size(min = 8, message = "Password must be at least 8 characters long") | ||
| private String password; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.example.tech_interview_buddy.dto.request; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.AllArgsConstructor; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class UserLoginRequest { | ||
| @NotBlank(message = "Username is required") | ||
| private String username; | ||
|
|
||
| @NotBlank(message = "Password is required") | ||
| private String password; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.tech_interview_buddy.dto.response; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class UserLoginResponse { | ||
| private String token; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RequiredArgsConstructor의 단점은 어떤게 있을까요 ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
크게 3가지 정도 학습해서 답변드립니다