diff --git a/build.gradle b/build.gradle index 348d178..319e6ee 100644 --- a/build.gradle +++ b/build.gradle @@ -22,12 +22,15 @@ repositories { } dependencies { + implementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'mysql:mysql-connector-java' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation 'io.springfox:springfox-swagger-ui:3.0.0' compileOnly 'org.projectlombok:lombok' diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin index 970c288..be38f72 100644 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and b/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/src/main/java/com/moodstation/springboot/MoodstationApplication.java b/src/main/java/com/moodstation/springboot/MoodstationApplication.java index 0a5588c..97111dc 100644 --- a/src/main/java/com/moodstation/springboot/MoodstationApplication.java +++ b/src/main/java/com/moodstation/springboot/MoodstationApplication.java @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@SpringBootApplication public class MoodstationApplication { public static void main(String[] args) { SpringApplication.run(MoodstationApplication.class, args); diff --git a/src/main/java/com/moodstation/springboot/config/JwtAuthenticationFilter.java b/src/main/java/com/moodstation/springboot/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..6525963 --- /dev/null +++ b/src/main/java/com/moodstation/springboot/config/JwtAuthenticationFilter.java @@ -0,0 +1,34 @@ +package com.moodstation.springboot.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + // 헤더에서 JWT 를 받아옵니다. + String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); + // 유효한 토큰인지 확인합니다. + if (token != null && jwtTokenProvider.validateToken(token)) { + // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다. + Authentication authentication = jwtTokenProvider.getAuthentication(token); + // SecurityContext 에 Authentication 객체를 저장합니다. + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } +} + diff --git a/src/main/java/com/moodstation/springboot/config/JwtTokenProvider.java b/src/main/java/com/moodstation/springboot/config/JwtTokenProvider.java new file mode 100644 index 0000000..e685904 --- /dev/null +++ b/src/main/java/com/moodstation/springboot/config/JwtTokenProvider.java @@ -0,0 +1,73 @@ +package com.moodstation.springboot.config; + +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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; +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + + private String secretKey = "moodstationsecret"; + + // 토큰 유효시간 30일 + private long tokenValidTime = 30 * 24 * 60 * 60 * 1000L; + + private final UserDetailsService userDetailsService; + + // 객체 초기화, secretKey를 Base64로 인코딩한다. + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + // JWT 토큰 생성 + public String createToken(String userPk, List roles) { + Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위 + claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다. + Date now = new Date(); + return Jwts.builder() + .setClaims(claims) // 정보 저장 + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time + .signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과 + // signature 에 들어갈 secret값 세팅 + .compact(); + } + + // JWT 토큰에서 인증 정보 조회 + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + // 토큰에서 회원 정보 추출 + public String getUserPk(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값' + public String resolveToken(HttpServletRequest request) { + return request.getHeader("ACCESS-TOKEN"); + } + + // 토큰의 유효성 + 만료일자 확인 + public boolean validateToken(String jwtToken) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken); + return !claims.getBody().getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/moodstation/springboot/config/WebSecurityConfig.java b/src/main/java/com/moodstation/springboot/config/WebSecurityConfig.java new file mode 100644 index 0000000..f91ce90 --- /dev/null +++ b/src/main/java/com/moodstation/springboot/config/WebSecurityConfig.java @@ -0,0 +1,49 @@ +package com.moodstation.springboot.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationManager; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private final JwtTokenProvider jwtTokenProvider; + + // 암호화에 필요한 PasswordEncoder를 Bean 등록합니다 + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + // authenticationManager를 Bean 등록합니다. + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다. + .csrf().disable() // csrf 보안 토큰 disable처리. + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다. + .and() + .authorizeRequests() // 요청에 대한 사용권한 체크 + .antMatchers("/admin/**").hasRole("ADMIN") + .antMatchers("/user/**").hasRole("USER") + .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능 + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class); + // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다 + } +} diff --git a/src/main/java/com/moodstation/springboot/dto/UserLoginRequestDto.java b/src/main/java/com/moodstation/springboot/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..d2ac581 --- /dev/null +++ b/src/main/java/com/moodstation/springboot/dto/UserLoginRequestDto.java @@ -0,0 +1,27 @@ +package com.moodstation.springboot.dto; + +import com.moodstation.springboot.entity.User; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLoginRequestDto { + + private String email; + private String password; + + @Builder + public UserLoginRequestDto(String email, String password) { + this.email = email; + this.password = password; + } + + public User toEntity() { + return User.builder() + .email(email) + .password(password) + .build(); + } +} diff --git a/src/main/java/com/moodstation/springboot/dto/UserCreateResponseDto.java b/src/main/java/com/moodstation/springboot/dto/UserResponseDto.java similarity index 57% rename from src/main/java/com/moodstation/springboot/dto/UserCreateResponseDto.java rename to src/main/java/com/moodstation/springboot/dto/UserResponseDto.java index 192c2c9..1bd5fea 100644 --- a/src/main/java/com/moodstation/springboot/dto/UserCreateResponseDto.java +++ b/src/main/java/com/moodstation/springboot/dto/UserResponseDto.java @@ -5,14 +5,16 @@ import org.springframework.http.HttpStatus; @Getter -public class UserCreateResponseDto { +public class UserResponseDto { - private HttpStatus statusCode; + private int status; + private String message; private String email; private String nickname; - public UserCreateResponseDto(HttpStatus statusCode, UserCreateRequestDto entity) { - this.statusCode = statusCode; + public UserResponseDto(HttpStatus statusCode, User entity) { + this.status = statusCode.value(); + this.message = "회원가입 성공"; this.email = entity.getEmail(); this.nickname = entity.getNickname(); } diff --git a/src/main/java/com/moodstation/springboot/dto/UserCreateRequestDto.java b/src/main/java/com/moodstation/springboot/dto/UserSignupRequestDto.java similarity index 85% rename from src/main/java/com/moodstation/springboot/dto/UserCreateRequestDto.java rename to src/main/java/com/moodstation/springboot/dto/UserSignupRequestDto.java index ec44784..bcc1793 100644 --- a/src/main/java/com/moodstation/springboot/dto/UserCreateRequestDto.java +++ b/src/main/java/com/moodstation/springboot/dto/UserSignupRequestDto.java @@ -7,14 +7,14 @@ @Getter @NoArgsConstructor -public class UserCreateRequestDto { +public class UserSignupRequestDto { private String email; private String nickname; private String password; @Builder - public UserCreateRequestDto(String email, String nickname, String password) { + public UserSignupRequestDto(String email, String nickname, String password) { this.email = email; this.nickname = nickname; this.password = password; diff --git a/src/main/java/com/moodstation/springboot/entity/User.java b/src/main/java/com/moodstation/springboot/entity/User.java index b3caeb1..2f8ebf9 100644 --- a/src/main/java/com/moodstation/springboot/entity/User.java +++ b/src/main/java/com/moodstation/springboot/entity/User.java @@ -1,48 +1,69 @@ package com.moodstation.springboot.entity; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.springframework.format.annotation.DateTimeFormat; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; -import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; @Getter -@Setter @NoArgsConstructor +@AllArgsConstructor +@Builder @Entity -@Table(name = "user") -public class User { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(length = 100, nullable = false, unique = true) private String email; - @Column(nullable = false) + @Column(length = 20, nullable = false, unique = true) private String nickname; - @Column(nullable = false) + @Column(length = 30, nullable = false) private String password; - @Column(name = "created_at") - @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDateTime updatedAt; - - @Builder - public User(String email, String nickname, String password) { - this.email = email; - this.nickname = nickname; - this.password = password; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + @ElementCollection(fetch = FetchType.EAGER) + @Builder.Default + private List roles = new ArrayList<>(); + + @Override + public Collection getAuthorities() { + return this.roles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; } } diff --git a/src/main/java/com/moodstation/springboot/repository/UserRepository.java b/src/main/java/com/moodstation/springboot/repository/UserRepository.java index dbbd16c..a3ae780 100644 --- a/src/main/java/com/moodstation/springboot/repository/UserRepository.java +++ b/src/main/java/com/moodstation/springboot/repository/UserRepository.java @@ -7,9 +7,5 @@ public interface UserRepository extends JpaRepository { - // 이메일 중복 방지 Optional findByEmail(String email); - - // 닉네임 중복 방지 - Optional findByNickname(String nickname); } diff --git a/src/main/java/com/moodstation/springboot/service/UserService.java b/src/main/java/com/moodstation/springboot/service/UserService.java index adddce7..80b03f9 100644 --- a/src/main/java/com/moodstation/springboot/service/UserService.java +++ b/src/main/java/com/moodstation/springboot/service/UserService.java @@ -1,28 +1,24 @@ package com.moodstation.springboot.service; -import com.moodstation.springboot.dto.UserCreateRequestDto; -import com.moodstation.springboot.dto.UserCreateResponseDto; +import com.moodstation.springboot.dto.UserResponseDto; import com.moodstation.springboot.entity.User; import com.moodstation.springboot.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; - @RequiredArgsConstructor @Service -public class UserService { +public class UserService implements UserDetailsService { private final UserRepository userRepository; - // 회원가입 - public String save(UserCreateRequestDto requestDto) { - userRepository.save(User.builder() - .email(requestDto.getEmail()) - .nickname(requestDto.getNickname()) - .password(requestDto.getPassword()) - .build()); - - return "Success"; + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); } } diff --git a/src/main/java/com/moodstation/springboot/web/UserApiController.java b/src/main/java/com/moodstation/springboot/web/UserApiController.java index 5dda79b..8565ba2 100644 --- a/src/main/java/com/moodstation/springboot/web/UserApiController.java +++ b/src/main/java/com/moodstation/springboot/web/UserApiController.java @@ -1,29 +1,48 @@ package com.moodstation.springboot.web; -import com.moodstation.springboot.dto.UserCreateRequestDto; -import com.moodstation.springboot.dto.UserCreateResponseDto; +import com.moodstation.springboot.config.JwtTokenProvider; +import com.moodstation.springboot.dto.UserLoginRequestDto; +import com.moodstation.springboot.dto.UserSignupRequestDto; import com.moodstation.springboot.entity.User; -import com.moodstation.springboot.service.UserService; +import com.moodstation.springboot.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.util.Collections; + @RequiredArgsConstructor @RestController public class UserApiController { - private final UserService userService; - - @PostMapping("/api/v1/signup") - public UserCreateResponseDto signup(@RequestBody UserCreateRequestDto requestDto) { - if (userService.save(requestDto).equals("Success")) { - return new UserCreateResponseDto(HttpStatus.CREATED, requestDto); - } + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; - return new UserCreateResponseDto(HttpStatus.BAD_REQUEST, requestDto); + // 회원가입 + @PostMapping("/signup") + public Long signup(@RequestBody UserSignupRequestDto requestDto) { + return userRepository.save(User.builder() + .email(requestDto.getEmail()) + .nickname(requestDto.getNickname()) + .password(passwordEncoder.encode(requestDto.getPassword())) + .roles(Collections.singletonList("ROLE_USER")) // 최초 가입시 USER 로 설정 + .build()).getId(); } + // 로그인 + @PostMapping("/login") + public String login(@RequestBody UserLoginRequestDto requestDto) { + User member = userRepository.findByEmail(requestDto.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("가입되지 않은 E-MAIL 입니다.")); + + System.out.println(member.getPassword()); + System.out.println(requestDto.getPassword()); + if (!passwordEncoder.matches(requestDto.getPassword(), member.getPassword())) { + throw new IllegalArgumentException("잘못된 비밀번호입니다."); + } + return jwtTokenProvider.createToken(member.getUsername(), member.getRoles()); + } } diff --git a/src/test/java/com/moodstation/springboot/repository/UserRepositoryTest.java b/src/test/java/com/moodstation/springboot/repository/UserRepositoryTest.java new file mode 100644 index 0000000..363090f --- /dev/null +++ b/src/test/java/com/moodstation/springboot/repository/UserRepositoryTest.java @@ -0,0 +1,49 @@ +package com.moodstation.springboot.repository; + +import com.moodstation.springboot.entity.User; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class UserRepositoryTest { + + @Autowired + UserRepository userRepository; + + @After + public void cleanup() { + userRepository.deleteAll(); + } + + @Test + public void 유저회원가입_불러오기() { + //given + String email = "테스트 이메일"; + String nickname = "테스트 닉네임"; + String password = "테스트 비밀번호"; + + userRepository.save(User.builder() + .email(email) + .nickname(nickname) + .password(password) + .build()); + + //when + List userList = userRepository.findAll(); + + //then + User user = userList.get(0); + assertThat(user.getEmail()).isEqualTo(email); + assertThat(user.getNickname()).isEqualTo(nickname); + assertThat(user.getPassword()).isEqualTo(password); + } +}