사용자는 회원가입을 진행
사용자의 정보를 저장할 때, PasswordEncoder를 사용하여 비밀번호를 암호화 한 후 저장
사용자는 로그인을 진행
사용자 인증을 성공하면,
사용자의 정보를 사용하여 JWT 토큰을 생성하고 Header에 추가하여 반환
Client 는 이를 쿠키저장소에 저장
사용자는 게시글 작성과 같은 요청을 진행할 때, 발급받은 JWT 토큰을 같이 보낸다.
서버는 JWT 토큰을 검증하고,
Spring Security 에 등록한 Custom Security Filter(토큰의 정보를 사용하여 사용자의 인증을 진행) 를 사용하여 인증/인가를 처리
Custom Security Filter에서 SecurityContextHolder 에 인증을 완료한 사용자의 상세 정보를 저장하는데,
이를 통해 Spring Security 에 인증이 완료 되었다는 것을 알려준다.
→ 즉, JWT 토큰과 Custom Security Filter를 통해, 사용자를 로그인이 된 상태로 유지
Spring Security 을 위한 dependencies 설치
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation group: 'org.json', name: 'json', version: '20220924'
// JWT 형식의 토큰을 발행, 검증
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
}
tasks.named('test') {
useJUnitPlatform()
}
user 등... 의 정보를 담는다.
//폴더 생성
@Getter
@Entity
@NoArgsConstructor
public class Folder {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
public Folder(String name, User user) {
this.name = name;
this.user = user;
}
}
@Getter
@Setter
@Entity // DB 테이블 역할을 합니다.
@NoArgsConstructor
public class Product extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // ID가 자동으로 생성 및 증가합니다.
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String image;
@Column(nullable = false)
private String link;
@Column(nullable = false)
private int lprice; //itemDto 의 lprice 값을 가져온 것을 기존의 lprice 쪽으로 update
@Column(nullable = false)
private int myprice;
@Column(nullable = false)
private Long userId; //userId 를 넣는다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
//관심상품에 폴더 추가 구현
//ManyToMany 가 단방향으로 걸려있음
//문제점? 우리가 직접 만든 테이블이 아니라, JPA 가 N:N 으로 풀어주기 위해 중간 테이블을 만듦
//왜 문제인가? 중간 테이블에는 추가 정보를 넣을 수 없음, 예상하지 못한 쿼리들이 JPA 에서 자동으로 나갈 수 있음
//해결법? OneToMany, OneToMany, ManyToOne 으로 풀어서 사용
@ManyToMany
private List<Folder> folderList = new ArrayList<>();
public Product(ProductRequestDto requestDto, Long userId) {
this.title = requestDto.getTitle();
this.image = requestDto.getImage();
this.link = requestDto.getLink();
this.lprice = requestDto.getLprice();
this.myprice = 0;
this.userId = userId; //생성자를 통해서 userId 도 같이 넣는다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
}
public void update(ProductMypriceRequestDto requestDto) {
this.myprice = requestDto.getMyprice();
}
public void updateByItemDto(ItemDto itemDto) {
//itemDto 의 lprice 값을 가져와서,
this.lprice = itemDto.getLprice();
}
//관심상품에 폴더 추가 구현
public void addFolder(Folder folder) { //Folder 를 받아와서 folderList 안에 넣음
this.folderList.add(folder);
}
}
Entity와 UserDetails는 구분할 수도 같은 클래스에서 관리할 수도 있음
예를 들면, public class User implements UserDetails
이렇게 상속받음으로써
//폴더 생성
@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
@OneToMany
List<Folder> folders = new ArrayList<>();
public User(String username, String password, String email, UserRoleEnum role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
}
user 의 권한 설정을 위한 enum
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
엔티티에서 해당 Timestamped 객체를 상속받음으로써
조회한 해당 엔티티의 값 변경 시, 시간 자동 생성/수정 되도록 함
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
// 생성 시간
@CreatedDate // Entity가 생성되어 저장될 때 시간이 자동으로 저장
@Column(updatable = false)
private LocalDateTime createdAt;
// 수정 시간
@LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동으로 저장
@Column
private LocalDateTime modifiedAt;
}
JpaRepository를 상속받음으로써
해당 엔티티를 대상으로 DB 에 접근하기 위한 인터페이스
//폴더 생성
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUser(User user);
//중복 폴더 생성 이슈 해결
List<Folder> findAllByUserAndNameIn(User user, List<String> names);
}
public interface ProductRepository extends JpaRepository<Product, Long> {
//UserId 를 통해, userId 가 동일한 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
//Page 객체로 받는다
Page<Product> findAllByUserId(Long userId, Pageable pageable);
//Product 의 id 와 userId 가 일치하는 Product 를 가져온다 (관심상품 조회를 위해서 user 와 product 의 연관관계를 짓기위해)
Optional<Product> findByIdAndUserId(Long id, Long userId);
Page<Product> findAll(Pageable pageable);
//폴더 별 관심상품 조회
Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable pageable);
//같은 폴더 내에 상품 중복 생성 이슈 해결
Optional<Product> findByIdAndFolderList_Id(Long productId, Long folderId);
}
//회원가입 구현
//UserRepository 가 필요한 이유? user 를 검증하려면 UserRepository 에 접근해서 DB 에 갔다와야 함
public interface UserRepository extends JpaRepository<User, Long> {
// 회원 중복 확인
Optional<User> findByUsername(String username);
}
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// h2-console 사용 및 resources 접근 허용 설정
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toH2Console())
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/api/user/**").permitAll()
.antMatchers("/api/search").permitAll()
.antMatchers("/api/shop").permitAll()
.anyRequest().authenticated()
// JWT 인증/인가를 사용하기 위한 설정
.and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
http.formLogin().loginPage("/api/user/login-page").permitAll();
http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
return http.build();
}
}
Spring Security 관련 설정 + 권한 설정
@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();
}
/* antMatchers(HttpMethod.GET,"/api/blogs/**").permitAll()
→ 이렇게 하면 GET 방식으로도 받아올 수 있게 된다.*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // "/admin/**", "/user/**" 형식의 URL로 들어오는 요청에 대해 인증을 요구
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/**").permitAll();
}
}
WebSecurityConfigurerAdapter 를 상속받고, @EnableWebSecurity 를 달아줌으로써
Spring Security Filter Chain 을 사용한다는 것을 명시
→ Spring Security를 사용이 가능해짐
++ 추가)
회원가입
···
@Override
protected void configure(HttpSecurity http) throws Exception {
// 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
http.csrf()
.ignoringAntMatchers("/user/**");
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 회원 관리 처리 API 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
// 그 외 어떤 요청이든 '인증'
.anyRequest().authenticated()
.and()
// 로그인 기능
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.failureUrl("/user/login?error")
.permitAll()
.and()
// 로그아웃 기능
.logout()
.permitAll();
}
}
로그아웃
···
@Override
protected void configure(HttpSecurity http) throws Exception {
// 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
http.csrf()
.ignoringAntMatchers("/user/**");
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 회원 관리 처리 API 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
// 그 외 어떤 요청이든 '인증'
.anyRequest().authenticated()
.and()
// [로그인 기능]
.formLogin()
// 로그인 View 제공 (GET /user/login)
.loginPage("/user/login")
// 로그인 처리 (POST /user/login)
.loginProcessingUrl("/user/login")
// 로그인 처리 후 성공 시 URL
.defaultSuccessUrl("/")
// 로그인 처리 후 실패 시 URL
.failureUrl("/user/login?error")
.permitAll()
.and()
// [로그아웃 기능]
.logout()
// 로그아웃 처리 URL
.logoutUrl("/user/logout")
.permitAll();
}
}
필터에서 토큰 인증 작업 진행
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
//request: 토큰을 가져온다
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
//로그인, 회원가입은 인증이 필요 없음 --> 그래서 Token 이 Header 에 들어가 있지 않음 --> 이런 처리(if) 해주지 않으면, 토큰을 검증하는 부분에서 exception 발생해버림
//인증이 필요없는 URL 는 if 문이 수행되지 않고, 다음 Filter 로 이동
if(token != null) {
//validateToken() 메소드를 실행해서 false 가 된다면,
if(!jwtUtil.validateToken(token)){
//jwtExceptionHandler 를 실행
jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
return;
}
//Token 에 문제가 없다면, 여기를 수행
//getUserInfoFromToken: Token 에서 UserInfo 유저의 정보를 가져온다
Claims info = jwtUtil.getUserInfoFromToken(token);
setAuthentication(info.getSubject());
}
filterChain.doFilter(request,response);
}
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext(); //SecurityContext context 안에 인증한 객체가 들어가게 됨
Authentication authentication = jwtUtil.createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
//위의 if(token != null) 여기에서 Token 에 대한 오류가 발생했을 때, Exception 한 결과값을 Client 에게 넘긴다
//validateToken() 메소드를 실행해서 false 가 됐다면, jwtExceptionHandler() 메소드 실행 --> Client 로 반환
public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
try {
//ObjectMapper 를 통해서 변환한다
String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
@Slf4j
@Component //빈 등록을 위해
@RequiredArgsConstructor
public class JwtUtil { //빈이 등록됐다는 '나뭇잎 모양' 확인 가능
private final UserDetailsServiceImpl userDetailsService;
//토큰 생성에 필요한 값
//Authorization: Bearer <JWT> 에서 Header 에 들어가는 KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
//사용자 권한도 Token 안에 넣는데, 이를 가져올 때 사용하는 KEY 값
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
//Token 을 만들 때, 앞에 들어가는 부분
private static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
//밀리세컨드 기준. 60 * 1000L는 1분. 60 * 60 * 1000L는 1시간
private static final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}") //@Value() 안에 application.properties 에 넣어둔 KEY 값(jwt.secret.key=7ZWt7ZW0O...pA=)을 넣으면, 가져올 수 있음
private String secretKey;
private Key key; //Key 객체: Token을 만들 때 넣어줄 KEY 값
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct //처음에 객체가 생성될 떄, 초기화하는 함수 --> 구글링
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey); //Base64로 인코딩되어 있는 것을, 값을 가져와서 디코드하고, byte 배열로 반환
key = Keys.hmacShaKeyFor(bytes); //반환된 bytes 를 hmacShaKeyFor() 메서드를 사용해서 Key 객체에 넣기
}
//Header 에서 Token 가져오기
public String resolveToken(HttpServletRequest request) {
//HttpServletRequest request 객체 안에 들어간 Token 값을 가져옴
//() 안에는 어떤 KEY 를 가져올지 정할 수 있음(여기선 AUTHORIZATION_HEADER 안에 있는 KEY 의 값을 가져옴)
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
//그리고, 가져온 코드가 있는지, BEARER_PREFIX 로 시작하는지 확인
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
//substring() 메소드를 사용해서, 앞의 일곱 글자를 지워줌 --> 앞의 일곱 글자는 Token 과 관련 없는 String 값이므로
return bearerToken.substring(7);
}
return null;
}
//JWT 생성
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX + //Token 앞에 들어가는 부분임
//실제로 만들어지는 부분
Jwts.builder()
//setSubject 라는 공간 안에 username 를 넣는다
.setSubject(username)
//claim 이라는 공간 안에 AUTHORIZATION_KEY 사용자의 권한을 넣고(이 권한을 가져올 때는 지정해둔 auth KEY 값을 사용해서 넣음)
.claim(AUTHORIZATION_KEY, role)
//이 Token 을 언제까지 유효하게 가져갈건지. date: 위의 Date date = new Date() 에서 가져온 부분
//getTime(): 현재 시간
//+ TOKEN_TIME: 우리가 지정해 둔 시간(TOKEN_TIME = 60 * 60 * 1000L)을 더한다 = 즉, 지금 기준으로 언제까지 가능한지 결정해줌
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
//Token 이 언제 만들어졌는지 넣는 부분
.setIssuedAt(date)
//secret key 를 사용해서 만든 key 객체와
//어떤 암호화 알고리즘을 사용해서 암호화할것인지 지정(SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256)
.signWith(key, signatureAlgorithm)
//반환
.compact();
}
//JWT 검증
// 토큰 검증
public boolean validateToken(String token) {
try {
//Jwts: 위에서 JWT 생성 시 사용했던 것
//parserBuilder(): 검증 방법
//setSigningKey(key): Token 을 만들 때 사용한 KEY
//parseClaimsJws(token): 어떤 Token 을 검증할 것인지
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); //이 코드만으로 내부적으로 검증 가능
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
//JWT 에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기 --> 위에서 validateToken() 으로 토큰을 검증했기에 이 코드를 사용할 수 있는 것
public Claims getUserInfoFromToken(String token) {
//getBody(): Token?? 안에 들어있는 정보를 가져옴
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// 인증 객체 생성
public Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username); //userDetailsService 에서 User 가 있는지 없는지 확인
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
UserDetailsService 를 상속받음으로써, 토큰에 저장된 유저 정보를 활용
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
return new UserDetailsImpl(user, user.getUsername());
}
}
Spring Security는 UserDetails 객체를 통해 유저 정보(권한 정보)를 관리
public class UserDetailsImpl implements UserDetails {
private final User user;
private final String username;
public UserDetailsImpl(User user, String username) {
this.user = user;
this.username = username;
}
public User getUser() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
controller 에서 회원가입 및 로그인 진행
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.FolderRequestDto;
import com.sparta.myselectshop.entity.Folder;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.security.UserDetailsImpl;
import com.sparta.myselectshop.service.FolderService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class FolderController {
private final FolderService folderService;
@PostMapping("/folders")
public List<Folder> addFolders(
@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
List<String> folderNames = folderRequestDto.getFolderNames();
System.out.println("======================================================");
System.out.println("user.getUsername() = " + userDetails.getUsername());
System.out.println("user.getUser() = " + userDetails.getUser());
System.out.println("user.getUser().getPassword() = " + userDetails.getUser().getPassword());
System.out.println("user.getUser().getId() = " + userDetails.getUser().getId());
System.out.println("======================================================");
//User 객체를 그대로 보내지 않고, userDetails 에 들어있던 Username 을 보냄
return folderService.addFolders(folderNames, userDetails.getUsername());
}
// 회원이 등록한 모든 폴더 조회
@GetMapping("/folders")
public List<Folder> getFolders(
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
return folderService.getFolders(userDetails.getUser());
}
// 회원이 등록한 폴더 내 모든 상품 조회
@GetMapping("/folders/{folderId}/products")
public Page<Product> getProductsInFolder(
@PathVariable Long folderId,
@RequestParam int page,
@RequestParam int size,
@RequestParam String sortBy,
@RequestParam boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
return folderService.getProductsInFolder(
folderId,
page-1,
size,
sortBy,
isAsc,
userDetails.getUser()
);
}
}
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.ProductMypriceRequestDto;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.dto.ProductResponseDto;
import com.sparta.myselectshop.entity.Product;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.security.UserDetailsImpl;
import com.sparta.myselectshop.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
// 관심 상품 등록하기
@Secured(UserRoleEnum.Authority.ADMIN)
@PostMapping("/products")
//@AuthenticationPrincipal UserDetailsImpl userDetails: 인증 객체에 담긴 UserDetailsImpl 을 파라미터로 받음
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
// 응답 보내기
return productService.createProduct(requestDto, userDetails.getUser());
}
// 관심 상품 조회하기
@GetMapping("/products")
public Page<Product> getProducts(
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
// 응답 보내기
return productService.getProducts(userDetails.getUser(), page-1, size, sortBy, isAsc);
}
// 관심 상품 최저가 등록하기
@PutMapping("/products/{id}")
public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
// 응답 보내기 (업데이트된 상품 id)
return productService.updateProduct(id, requestDto, userDetails.getUser());
}
// 상품에 폴더 추가
@PostMapping("/products/{productId}/folder")
public Long addFolder(
@PathVariable Long productId,
@RequestParam Long folderId,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
Product product = productService.addFolder(productId, folderId, userDetails.getUser());
return product.getId();
}
}
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.security.UserDetailsImpl;
import com.sparta.myselectshop.service.FolderService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequiredArgsConstructor
@RequestMapping("/api")
public class ShopController {
private final FolderService folderService;
@GetMapping("/shop")
public ModelAndView shop() {
return new ModelAndView("index");
}
// 로그인 한 유저가 메인페이지를 요청할 때 가지고있는 폴더를 반환
@GetMapping("/user-folder")
public String getUserInfo(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
model.addAttribute("folders", folderService.getFolders(userDetails.getUser()));
return "index :: #fragment";
}
// 로그인 한 유저가 메인페이지를 요청할 때 유저의 이름 반환
@GetMapping("/user-info")
@ResponseBody
//UserDetailsImpl 에 들어 있는 Username 을 반환
public String getUserName(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return userDetails.getUsername();
}
}
package com.sparta.myselectshop.controller;
import com.sparta.myselectshop.dto.LoginRequestDto;
import com.sparta.myselectshop.dto.SignupRequestDto;
import com.sparta.myselectshop.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletResponse;
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@GetMapping("/signup")
public ModelAndView signupPage() {
return new ModelAndView("signup");
}
@GetMapping("/login-page")
public ModelAndView loginPage() {
return new ModelAndView("login");
}
@PostMapping("/signup")
public String signup(SignupRequestDto signupRequestDto) {
userService.signup(signupRequestDto);
return "redirect:/api/user/login-page";
}
@ResponseBody
@PostMapping("/login")
public String login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
userService.login(loginRequestDto, response);
return "success";
}
@GetMapping("/forbidden")
public ModelAndView getForbidden() {
return new ModelAndView("forbidden");
}
@PostMapping("/forbidden")
public ModelAndView postForbidden() {
return new ModelAndView("forbidden");
}
}
dto
FolderRequestDto, ProductMypriceRequestDto, ProductRequestDto, ProductResponseDto,
SecurityExceptionDto, SignupRequestDto, LoginRequestDto
@Scheduler
@Slf4j
@Component // 해당 클래스를 스프링이 빈으로 인식할 수 있도록
@RequiredArgsConstructor
public class Scheduler {
private final NaverApiService naverApiService;
private final ProductService productService;
private final ProductRepository productRepository;
// 초, 분, 시, 일, 월, 주 순서
@Scheduled(cron = "0 0 1 * * *") // 해석 : 새벽 1시에 매번 자동으로 이 메소드가 실행됨
public void updatePrice() throws InterruptedException { // updatePrice() 메소드 : 현재시간을 출력
log.info("가격 업데이트 실행");
//productRepository 로 모든 product 를 가져온다
List<Product> productList = productRepository.findAll();
//for 문 돌면서,
for (Product product : productList) {
// 1초에 한 상품 씩 조회합니다 (NAVER 제한)
TimeUnit.SECONDS.sleep(1);
// product 에서 title 을 가져온다
String title = product.getTitle();
// title 을 통해서, naverApiService 를 사용해서 itemDtoList 를 가져온다
List<ItemDto> itemDtoList = naverApiService.searchItems(title);
// 가장 상단에 있는 item 을 가져온다
ItemDto itemDto = itemDtoList.get(0);
// i 번째 관심 상품 정보를 업데이트합니다.
// product 의 id 도 가져와서, 가장 상단에 있는 item 과 id 를 통해 update 를 실시
Long id = product.getId();
productService.updateBySearch(id, itemDto);
}
}
}
참고: SPRING SECURITY + JWT 회원가입, 로그인 기능 구현
참고: [Spring] 생성시간, 수정시간 관리
참고: [Spring] @Scheduled - 스프링에서 주기적인 작업을 진행하는 법
참고: [Spring Boot] @Scheduled을 이용해 일정 시간 마다 코드 실행하기