묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
MappingJackson2HttpMessageConverter가 JsonConverter인가요?
[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)[질문 내용]MappingJackson2HttpMessageConverter가 JsonConverter인가요?
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
h2 데이터베이스 연결시 포트번호 오류
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]현재 8082번은 다른 프로젝트 때문에 application.properties에서server.port=8083 로8083번으로 바꿔놨습니다. 여기서 h2.bat 파일을 실행했을 때이렇게 이미 포트 번호가 사용 중이라는 오류가 뜹니다. 그래서 cmd에서netstat -ano | findstr :8083로태스크의 pid를 찾아 해당 태스크를 죽이고,H2 콘솔을 실행시키면당연히 스프링부트에서 이미 포트 번호가 사용 중이라는 오류가 뜹니다. 부트에서 포트 번호를 사용하면 H2에서 안되고H2에서 포트 번호를 사용하면 부트에서 안됩니다. 서버를 통해서 DB에 접속하려는 건데 왜 이런 오류가 뜰까요..서버에서 정상적으로 h2를 연결하고 싶습니다. 혹시 application.properties에서 server.port= 명령어로 임의로 포트를 바꿨기 때문인가요?
-
미해결스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
MemberServiceTest의 join()에 대해서 질문 있습니다.
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)[질문 내용]여기에 질문 내용을 남겨주세요. MemberServiceTest의 join() 부분을 시청하던 도중 의문점이 생겨 질문드립니다. 현재 저희는 MemberService의 클래스가 잘 만들어졌는지 테스트를 하는 과정에서 join, findMembers, findOne을 테스트합니다. 이런 과정에서 join을 테스트할 때, 아직 검증하지 않은 findOne을 통해서 join을 테스트하는 것이 옳은 방법인지 궁금합니다.기존에 아직 검증되지 않은 메서드를 사용해도 무방한 것일까요?? 만일 findOne이 잘 정의되어있지 않다면 아무리 잘 정의된 join도 Test에서 오류를 일으킬 것 같아서 질문드립니다.
-
미해결스프링부트 시큐리티 & JWT 강의
소셜 로그인 후 JWT 발급
SecurityConfigpackage com.example.project1.config.security; import com.example.project1.config.jwt.JwtAccessDeniedHandler; import com.example.project1.config.jwt.JwtAuthenticationEntryPoint; import com.example.project1.config.jwt.JwtProvider; import com.example.project1.config.oauth2.PrincipalOauth2UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import java.util.HashMap; import java.util.Map; @Configuration @RequiredArgsConstructor @EnableWebSecurity // @EnableGlobalMethodSecurity 어노테이션은 Spring Security에서 메서드 수준의 보안 설정을 활성화하는데 사용되는 어노테이션입니다. //@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig { private final JwtProvider jwtProvider; private final PrincipalOauth2UserService principalOauth2UserService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 스프링 시큐리티에서 제공하는 로그인 페이지를 안쓰기 위해 .httpBasic().disable() // JWt 방식을 제대로 쓰려고 하면 프론트엔드가 분리된 환경을 가정하고 해야합니다. .csrf().disable() .formLogin().disable() .logout().disable() // JWT 방식은 세션저장을 사용하지 않기 때문에 꺼줍니다. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .authorizeRequests() .antMatchers("/api/v1/boards/write") .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/boards/modify") .access("hasRole('ROLE_USER')") .antMatchers("/api/v1/boards/remove") .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/admin/**") .access("hasRole('ROLE_ADMIN')") // /success-oauth 엔드포인트에 대해 인증된 사용자만 접근 가능하도록 설정 // .antMatchers("/success-oauth").authenticated() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/swagger-ui/**").permitAll() .antMatchers("/api/v1/users/**").permitAll(); http // JWT Token을 위한 Filter를 아래에서 만들어 줄건데, // 이 Filter를 어느위치에서 사용하겠다고 등록을 해주어야 Filter가 작동이 됩니다. // security 로직에 JwtFilter 등록 // .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) .apply(new JwtSecurityConfig(jwtProvider)); // 에러 방지 http .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .accessDeniedHandler(new JwtAccessDeniedHandler()); // oauth2 http // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다. .oauth2Login() // .defaultSuccessUrl("/success-oauth") // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userInfoEndpoint() // OAuth2 로그인 성공 시, 후작업을 진행할 서비스 .userService(principalOauth2UserService) .and() .defaultSuccessUrl("/success-oauth"); return http.build(); } @Bean PasswordEncoder passwordEncoder() { String idForEncode = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); return new DelegatingPasswordEncoder(idForEncode, encoders); } }JwtProviderpackage com.example.project1.config.jwt; import com.example.project1.domain.jwt.TokenDTO; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import io.jsonwebtoken.security.Keys; import javax.xml.bind.DatatypeConverter; import java.security.Key; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @Slf4j @Component public class JwtProvider { private static final String AUTHORITIES_KEY = "auth"; @Value("${jwt.access.expiration}") private long accessTokenTime; @Value("${jwt.refresh.expiration}") private long refreshTokenTime; private Key key; public JwtProvider( @Value("${jwt.secret_key}") String secret_key) { byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secret_key); this.key = Keys.hmacShaKeyFor(secretByteKey); } // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메소드 public TokenDTO createToken(Authentication authentication) { // 권한 가져오기 // authentication 객체에서 권한 정보(GrantedAuthority)를 가져와 문자열 형태로 변환한 후, // 쉼표로 구분하여 조인한 결과를 authorities 변수에 저장합니다. 따라서 authorities는 권한 정보를 문자열 형태로 가지게 됩니다. // 권한 정보를 문자열로 변환하여 클레임에 추가하는 방식 String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); long now = (new Date()).getTime(); Date now2 = new Date(); // AccessToken 생성 Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() // 내용 sub : 유저의 이메일 // 토큰 제목 .setSubject(authentication.getName()) .setIssuedAt(now2) // 클레임 id : 유저 ID .claim(AUTHORITIES_KEY, authorities) // 내용 exp : 토큰 만료 시간, 시간은 NumericDate 형식(예: 1480849143370)으로 하며 // 항상 현재 시간 이후로 설정합니다. .setExpiration(accessTokenExpire) // 서명 : 비밀값과 함께 해시값을 ES256 방식으로 암호화 .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("accessToken : " + accessToken); // RefreshToken 생성 Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .setIssuedAt(now2) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("refreshToken : " + refreshToken); return TokenDTO.builder() .grantType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) .accessTokenTime(accessTokenExpire) .refreshTokenTime(refreshTokenExpire) // principalDeatails에서 getUserName 메소드가 반환한 것을 담아준다. // 이메일을 반환하도록 구성했으니 이메일이 반환됩니다. .userEmail(authentication.getName()) .build(); } // 소셜 로그인 성공시 JWT 발급 public TokenDTO createToken2(UserDetails userDetails) { long now = (new Date()).getTime(); Date now2 = new Date(); // userDetails.getAuthorities()는 사용자의 권한(authorities) 정보를 가져오는 메서드입니다. // claims.put("roles", userDetails.getAuthorities()) 코드는 사용자의 권한 정보를 클레임에 추가하는 것입니다. // 클레임에는 "roles"라는 키로 사용자의 권한 정보가 저장되며, 해당 권한 정보는 JWT의 페이로드 부분에 포함됩니다. Claims claims = Jwts.claims().setSubject(userDetails.getUsername()); claims.put("roles", userDetails.getAuthorities()); // access token Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() .setSubject(userDetails.getUsername()) .setClaims(claims) .setIssuedAt(now2) .setExpiration(accessTokenExpire) .signWith(key,SignatureAlgorithm.HS256) .compact(); // RefreshToken 생성 Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); return TokenDTO.builder() .grantType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) .userEmail(userDetails.getUsername()) .build(); } // accessToken 생성 // 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성하는 로직을 구현 public TokenDTO createAccessToken(String refreshToken, List<GrantedAuthority> authorities) { Long now = (new Date()).getTime(); Date now2 = new Date(); Date accessTokenExpire = new Date(now + this.accessTokenTime); String userEmail = extractUserEmailFromToken(refreshToken); String accessToken = Jwts.builder() .setIssuedAt(now2) .setSubject(userEmail) .setExpiration(accessTokenExpire) .claim(AUTHORITIES_KEY, authorities ) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("accessToken : " + accessToken); return TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .userEmail(userEmail) .build(); } // 리프레시 토큰의 유효성을 검증하는 로직을 구현 // 예를 들어, 토큰 서명 검증 및 만료 시간 확인 등을 수행 public boolean validateRefreshToken(String refreshToken) { try { // 토큰의 유효성을 검증하는 로직을 구현 // 예를 들어, 토큰의 서명을 확인하고 만료 시간을 검사합니다. // 유효한 토큰인 경우 true를 반환하고, 그렇지 않은 경우 false를 반환합니다. Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken); return true; } catch (Exception e) { return false; } } // 리프레시 토큰에서 사용자 이메일을 추출하는 로직을 구현 // 예를 들어, 토큰의 특정 클레임에서 사용자 이메일을 추출하여 반환 public String extractUserEmailFromToken(String refreshToken) { Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody(); // 사용자 이메일을 추출하는 로직을 구현하여 결과를 반환 return claims.getSubject(); } // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 코드 // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴 // 인증 정보 조회 public Authentication getAuthentication(String token) { // 토큰 복호화 메소드 Claims claims = parseClaims(token); if(claims.get("auth") == null) { throw new RuntimeException("권한 정보가 없는 토큰입니다."); } // 클레임 권한 정보 가져오기 Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // UserDetails 객체를 만들어서 Authentication 리턴 User principal = new User(claims.getSubject(), "", authorities); return new UsernamePasswordAuthenticationToken(principal, token, authorities); } private Claims parseClaims(String token) { try { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { log.info("ExpiredJwtException : " + e.getMessage()); log.info("ExpiredJwtException : " + e.getClaims()); return e.getClaims(); } } // 토큰의 유효성 검증을 수행 public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("잘못된 JWT 서명입니다."); } catch (ExpiredJwtException e) { log.info("만료된 JWT 토큰입니다."); } catch (UnsupportedJwtException e) { log.info("지원되지 않는 JWT 토큰입니다."); }catch (IllegalArgumentException e) { log.info("JWT 토큰이 잘못되었습니다."); } return false; } }JwtAuthenticationFilterpackage com.example.project1.config.jwt; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; 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; // 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로 // UsernamePasswordAuthenticationFiler 이전에 실행된다. // 이전에 실행된다는 뜻은 JwtAuthenticationFilter 를 통과하면 // UsernamePasswordAuthenticationFilter 이후의 필터는 통과한 것으로 본다는 뜻이다. // 쉽게 말해서, Username + Password 를 통한 인증을 Jwt 를 통해 수행한다는 것이다. // JWT 방식은 세션과 다르게 Filter 하나를 추가해야 합니다. // 이제 사용자가 로그인을 했을 때, Request 에 가지고 있는 Token 을 해석해주는 로직이 필요합니다. // 이 역할을 해주는것이 JwtAuthenticationFilter입니다. // 세부 비즈니스 로직들은 TokenProvider에 적어둡니다. 일종의 service 클래스라고 생각하면 편합니다. // 1. 사용자의 Request Header에 토큰을 가져옵니다. // 2. 해당 토큰의 유효성 검사를 실시하고 유효하면 // 3. Authentication 인증 객체를 만들고 // 4. ContextHolder에 저장해줍니다. // 5. 해당 Filter 과정이 끝나면 이제 시큐리티에 다음 Filter로 이동하게 됩니다. @RequiredArgsConstructor @Slf4j public class JwtAuthenticationFilter extends GenericFilterBean { public static final String HEADER_AUTHORIZATION = "Authorization"; private final JwtProvider jwtProvider; // doFilter는 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // Request Header에서 JWT 토큰을 추출 // 요청 헤더에서 JWT 토큰을 추출하는 역할 String jwt = resolveToken(httpServletRequest); String requestURI = httpServletRequest.getRequestURI(); if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)){ // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장 Authentication authentication = jwtProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("Security Context에 '{}' 인증 정보를 저장했습니다., uri : {}", authentication.getName(), requestURI); } else { log.debug("유효한 JWT 토큰이 없습니다. uri : {}", requestURI); } chain.doFilter(request, response); } // Request Header 에서 토큰 정보를 꺼내오기 위한 메소드 // HEADER_AUTHORIZATION로 정의된 헤더 이름을 사용하여 토큰을 찾고, // 토큰이 "Bearer "로 시작하는 경우에만 실제 토큰 값을 반환 private String resolveToken(HttpServletRequest httpServletRequest) { String bearerToken = httpServletRequest.getHeader(HEADER_AUTHORIZATION); if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } else { return null; } } }JwtAccessDeniedHandler, JwtAuthenticationEntryPoint 생략 PrincipalDetailspackage com.example.project1.config.auth; import com.example.project1.entity.member.MemberEntity; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @Setter @Getter @ToString public class PrincipalDetails implements UserDetails, OAuth2User { private MemberEntity member; private Map<String, Object> attributes; // 일반 로그인 public PrincipalDetails(MemberEntity member) { this.member = member; } // OAuth2 로그인 public PrincipalDetails(MemberEntity member, Map<String, Object> attributes) { this.member = member; this.attributes = attributes; } // 해당 유저의 권한을 리턴하는 곳 @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> collection = new ArrayList<>(); collection.add(new SimpleGrantedAuthority("ROLE_" + member.getUserType().toString())); return collection; } // 사용자 패스워드를 반환 @Override public String getPassword() { return member.getUserPw(); } // 사용자 이름 반환 @Override public String getUsername() { return member.getUserEmail(); } // 계정 만료 여부 반환 @Override public boolean isAccountNonExpired() { // 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 잠금 여부 반환 @Override public boolean isAccountNonLocked() { // true = 잠금되지 않음 return true; } // 패스워드의 만료 여부 반환 @Override public boolean isCredentialsNonExpired() { // 패스워드가 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 사용 가능 여부 반환 @Override public boolean isEnabled() { // 계정이 사용 가능한지 확인하는 로직 // true = 사용 가능 return true; } @Override public Map<String, Object> getAttributes() { return attributes; } @Override public String getName() { return null; } }PrincipalDetailsServicepackage com.example.project1.config.auth; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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; // http://localhost:8080/login ← 이 때 동작을 함 @Service @RequiredArgsConstructor @Slf4j public class PrincipalDetailsService implements UserDetailsService { private MemberRepository memberRepository; // 시큐리티 session = Authentication = UserDetails // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다. @Override public UserDetails loadUserByUsername(String userEmail) throws UsernameNotFoundException { MemberEntity member = memberRepository.findByUserEmail(userEmail); log.info("user : " + member); return new PrincipalDetails(member); } }OAuth2UserInfopackage com.example.project1.config.oauth2.provider; public interface OAuth2UserInfo { String getProviderId(); String getProvider(); String getEmail(); String getName(); }PrincipalOauth2UserServicepackage com.example.project1.config.oauth2; import com.example.project1.config.auth.PrincipalDetails; import com.example.project1.config.oauth2.provider.GoogleUserInfo; import com.example.project1.config.oauth2.provider.NaverUserInfo; import com.example.project1.config.oauth2.provider.OAuth2UserInfo; import com.example.project1.domain.member.MemberDTO; import com.example.project1.domain.member.UserType; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.Map; import java.util.Objects; import java.util.Optional; @Service @Slf4j @RequiredArgsConstructor public class PrincipalOauth2UserService extends DefaultOAuth2UserService { private final BCryptPasswordEncoder bCryptPasswordEncoder; private final MemberRepository memberRepository; // 구글로부터 받은 userReuest 데이터에 대한 후처리되는 함수 @Override public PrincipalDetails loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // registrationId로 어떤 OAuth로 로그인 했는지 확인가능 log.info("clientRegistration : " + userRequest.getClientRegistration() ); log.info("accessToken : " + userRequest.getAccessToken().getTokenValue() ); OAuth2User oAuth2User = super.loadUser(userRequest); // 구글 로그인 버튼 클릭 →구글 로그인 창 → 로그인 완료 → code 를 리턴(OAuth-Client 라이브러리) → AccessToken 요청 // userRequest 정보 → 회원 프로필 받아야함(loadUser 함수 호출) → 구글로부터 회원 프로필을 받아준다. log.info("getAttributes : " + oAuth2User.getAttributes()); // 회원가입을 강제로 진행 OAuth2UserInfo oAuth2UserInfo = null; if(userRequest.getClientRegistration().getRegistrationId().equals("google")) { log.info("구글 로그인 요청"); oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes()); } else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) { log.info("네이버 로그인 요청"); // 네이버는 response를 json으로 리턴을 해주는데 아래의 코드가 받아오는 코드다. // response={id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영} // 위의 정보를 NaverUserInfo에 넘기면 oAuth2UserInfo = new NaverUserInfo((Map)oAuth2User.getAttributes().get("response")); } else { log.info("구글과 네이버만 지원합니다."); } String provider = oAuth2UserInfo.getProvider(); String providerId = oAuth2UserInfo.getProviderId(); // 예) google_109742856182916427686 String userName = provider + "_" + providerId; String password = bCryptPasswordEncoder.encode("get"); String email = oAuth2UserInfo.getEmail(); UserType role = UserType.USER; MemberEntity member = memberRepository.findByUserEmail(email); if(member == null) { log.info("OAuth 로그인이 최초입니다."); member = MemberEntity.builder() .userName(userName) .userPw(password) .userEmail(email) .userType(role) .provider(provider) .providerId(providerId) .build(); memberRepository.save(member); } else { log.info("로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다."); } return new PrincipalDetails(member, oAuth2User.getAttributes()); } }GoogleUserInfopackage com.example.project1.config.oauth2.provider; import java.util.Map; public class GoogleUserInfo implements OAuth2UserInfo{ // getAttributes()를 받음 private Map<String, Object> attributes; public GoogleUserInfo(Map<String, Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String) attributes.get("sub"); } @Override public String getProvider() { return "google"; } @Override public String getEmail() { return (String) attributes.get("email"); } @Override public String getName() { return (String) attributes.get("name"); } }NaverUserInfopackage com.example.project1.config.oauth2.provider; import java.util.Map; public class NaverUserInfo implements OAuth2UserInfo{ // oauth2User.getAttributes()를 받음 private Map<String,Object> attributes; // PrincipalOauth2UserService에서 new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"))로 // Oauth2 네이버 로그인 정보를 받아온다. // → {id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영} public NaverUserInfo(Map<String, Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String)attributes.get("id"); } @Override public String getProvider() { return "naver"; } @Override public String getEmail() { return (String)attributes.get("email"); } @Override public String getName() { return (String)attributes.get("name"); } } MemberController @GetMapping("/success-oauth") public ResponseEntity<?> createTokenForGoogle(@AuthenticationPrincipal OAuth2User oAuth2User) { if(oAuth2User == null) { log.info("받아올 정보가 없습니다 ㅠㅠ"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body("정보가 없어...."); } else { log.info("oauth2User 정보를 받아오자 : " + oAuth2User); // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성합니다. ResponseEntity<TokenDTO> token = memberService.createToken(oAuth2User); log.info("token : " + token); return ResponseEntity.ok().body(token); } }TokenDTOpackage com.example.project1.domain.jwt; import com.example.project1.entity.jwt.TokenEntity; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import java.util.Date; @Getter @ToString @NoArgsConstructor public class TokenDTO { private Long id; // JWT 대한 인증 타입, 여기서는 Bearer를 사용하고 // 이후 HTTP 헤더에 prefix로 붙여주는 타입 private String grantType; private String accessToken; private String refreshToken; private String userEmail; private String nickName; private Long userId; private Date accessTokenTime; private Date refreshTokenTime; @Builder public TokenDTO(String grantType, String accessToken, String refreshToken, String userEmail, String nickName, Long userId, Date accessTokenTime, Date refreshTokenTime) { this.grantType = grantType; this.accessToken = accessToken; this.refreshToken = refreshToken; this.userEmail = userEmail; this.nickName = nickName; this.userId = userId; this.accessTokenTime = accessTokenTime; this.refreshTokenTime = refreshTokenTime; } public static TokenDTO toTokenDTO(TokenEntity tokenEntity) { TokenDTO tokenDTO = TokenDTO.builder() .grantType(tokenEntity.getGrantType()) .accessToken(tokenEntity.getAccessToken()) .refreshToken(tokenEntity.getRefreshToken()) .userEmail(tokenEntity.getUserEmail()) .nickName(tokenEntity.getNickName()) .userId(tokenEntity.getId()) .accessTokenTime(tokenEntity.getAccessTokenTime()) .refreshTokenTime(tokenEntity.getRefreshTokenTime()) .build(); return tokenDTO; } }TokenEntitypackage com.example.project1.entity.jwt; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import java.util.Date; @Entity @Getter @NoArgsConstructor @ToString public class TokenEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String grantType; private String accessToken; private String refreshToken; private String userEmail; private String nickName; private Long userId; private Date accessTokenTime; private Date refreshTokenTime; @Builder public TokenEntity(Long id, String grantType, String accessToken, String refreshToken, String userEmail, String nickName, Long userId, Date accessTokenTime, Date refreshTokenTime) { this.id = id; this.grantType = grantType; this.accessToken = accessToken; this.refreshToken = refreshToken; this.userEmail = userEmail; this.nickName = nickName; this.userId = userId; this.accessTokenTime = accessTokenTime; this.refreshTokenTime = refreshTokenTime; } }MemberServicepackage com.example.project1.service.member; import com.example.project1.config.jwt.JwtAuthenticationFilter; import com.example.project1.config.jwt.JwtProvider; import com.example.project1.domain.jwt.TokenDTO; import com.example.project1.domain.member.MemberDTO; import com.example.project1.domain.member.UserType; import com.example.project1.entity.jwt.TokenEntity; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.jwt.TokenRepository; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.*; @Service @RequiredArgsConstructor @Slf4j public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; private final AuthenticationManagerBuilder authenticationManagerBuilder; private final JwtProvider jwtProvider; private final TokenRepository tokenRepository; // 회원가입 public String signUp(MemberDTO memberDTO) throws Exception { try { MemberEntity byUserEmail = memberRepository.findByUserEmail(memberDTO.getUserEmail()); if (byUserEmail != null) { return "이미 가입된 회원입니다."; } // 아이디가 없다면 DB에 넣어서 등록 해준다. MemberEntity member = MemberEntity.builder() .userEmail(memberDTO.getUserEmail()) .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .userType(memberDTO.getUserType()) .provider(memberDTO.getProvider()) .providerId(memberDTO.getProviderId()) .build(); log.info("member : " + member); MemberEntity save = memberRepository.save(member); // MemberDTO memberDTO1 = MemberDTO.toMemberDTO(Optional.of(save)); return "회원가입에 성공했습니다."; } catch (Exception e) { log.error(e.getMessage()); throw e; // 예외를 던져서 예외 처리를 컨트롤러로 전달 } } // 아이디 조회 public MemberDTO search(Long userId) { Optional<MemberEntity> searchId = memberRepository.findById(userId); MemberDTO memberDTO = MemberDTO.toMemberDTO(searchId); return memberDTO; } // 로그인 public ResponseEntity<TokenDTO> login(String userEmail, String userPw) throws Exception { MemberEntity findUser = memberRepository.findByUserEmail(userEmail); log.info("findUser : " + findUser); if (findUser != null) { Authentication authentication = new UsernamePasswordAuthenticationToken(userEmail, userPw); TokenDTO token = jwtProvider.createToken(authentication); // // Login ID/PW를 기반으로 UsernamePasswordAuthenticationToken 생성 token = TokenDTO.builder() .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(findUser.getUserEmail()) .nickName(findUser.getNickName()) .userId(findUser.getUserId()) .build(); TokenEntity tokenEntity = TokenEntity.builder() .id(token.getId()) .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(token.getUserEmail()) .nickName(token.getNickName()) .userId(token.getUserId()) .build(); log.info("token : " + tokenEntity); tokenRepository.save(tokenEntity); return new ResponseEntity<>(token, HttpStatus.OK); } else { return null; } } // 회원정보 수정 public MemberDTO update(MemberDTO memberDTO) { MemberEntity member = MemberEntity.builder() .userEmail(memberDTO.getUserEmail()) .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .userType(memberDTO.getUserType()) .provider(memberDTO.getProvider()) .providerId(memberDTO.getProviderId()) .build(); memberRepository.save(member); // 제대로 DTO 값이 엔티티에 넣어졌는지 확인하기 위해서 // 엔티티에 넣어주고 다시 DTO 객체로 바꿔서 리턴을 해줬습니다. MemberDTO memberDto = MemberDTO.toMemberDTO(Optional.of(member)); log.info("memberDto : " + memberDto); return memberDto; } // 소셜 로그인 성공시 jwt 반환 // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성하는 메서드 public ResponseEntity<TokenDTO> createToken(OAuth2User oAuth2User) { String userEmail = oAuth2User.getAttribute("email"); log.info("userEmail : " + userEmail); MemberEntity findMember = memberRepository.findByUserEmail(userEmail); // 권한 정보 추출 List<GrantedAuthority> authorities = getAuthoritiesForUser(findMember); // UserDetails 객체 생성 (사용자의 아이디 정보를 활용) // 첫 번째 인자 : username 사용자 아이디 // 두 번째 인자 : 사용자의 비밀번호 // 세 번째 인자 : 사용자의 권한 정보를 담은 컬렉션 UserDetails userDetails = new User(userEmail, null, authorities); log.info("userDetails : " + userDetails); TokenDTO token = jwtProvider.createToken2(userDetails); log.info("token : " + token); return ResponseEntity.ok().body(token); } private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) { // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현 // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다. UserType role = member.getUserType(); // 사용자의 권한 정보를 가져오는 로직 (예시) List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role.name())); return authorities; } } 이렇게 했는데 PrincipalOauth2UserService에는 값이 잘 받아지는데 컨트롤러에서 @AuthenticationPrincipal OAuth2User oAuth2User으로 소셜 로그인 성공하면 정보를 뽑아서 JWT를 발급해주려고 하는데 소셜 로그인을 하고 log를 찍어보면 null이 뜹니다.... 어떻게 해야할까요
-
미해결자바 스프링 프레임워크(renew ver.) - 신입 프로그래머를 위한 강좌
스프링 레거시 프로젝트 생성 오류 질문드립니다.
스프링 레거시 프로젝트를 생성하는 와중에이런 오류가 떠서 해결이 안되네요.. 혹시 해결방법 아시는지 궁금합니다.구글링도 해보고 여러가지 다 해봤는데 모르겠어요.참고로 이클립스 버전은 2020-06 쓰고있고 jdk 1.8.0_151 쓰고있습니다.환경변수는 강의서에 나온 그대로 설정해 놓은 상태입니다.도와주세요
-
미해결실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
JPA> DDD 서로 다른 애그리거트 사이의 간접참조
안녕하세요 영한님! 프로젝트에 JPA 및 DDD를 적용하며 의문점이 생기는 부분이 있는데 물어볼 곳이 마땅치 않아서, 이렇게 질문글 올립니다. 구조(요약) - 모노리틱저희 프로젝트에서 다음과 같이 서로 다른 애그리거트 사이에는 간접참조를 통해 느슨하게 결합하여 사용하고 있습니다. 질문다음과 같이 설정하고 나니 몇 가지 의문점이 있습니다. 간접 참조를 통해 느슨한 결합을 하고 있는데, DB 에 따로 외래키 제약 조건을 걸어주어야하는가? 이 부분에 있어서 팀원과 의견이 달라서요. 저는 애그리거트를 나눈 목적이 결국 복잡도를 줄이기 위해서이고, 각각의 애그리거트 안에서 트랜잭션으로 관리가 되어야한다고 생각을 하는데.. 외래키를 설정하면 결국은 User 를 관리할 때 참조 무결성을 신경쓰면서 Trip 까지 고려를 해야돼서 좋지 않다고 생각합니다. 굳이 DB에서 외래키 제약을 걸지 않아도 이벤트를 통해 관리할 수 있다고 생각을 하기도 하구요. 팀원분은 어플리케이션보다는 변경점이 낮은 DB를 우선으로 하며 개발을 해야하고, 그렇기 때문에 반드시 외래키를 걸어주어야한다고 말씀하시는데, 무엇이 더 맞는 말인지 잘 모르겠습니다. 회원 탈퇴(User 삭제)가 이루어질 때 회원과 관련된 모든 Trip 삭제(벌크성 삭제 쿼리)가 이루어지도록 하기 위해서 이벤트를 활용하고 있는데, 이를 비동기적으로 처리해도 문제가 되지 않는가?@Async, @TransactionalEventListener 를 활용해서 회원탈퇴 요청시 회원 삭제가 커밋이 완료 되면 회원이 작성한 여행 정보를 지우는 비동기 이벤트를 발행하도록 하고 있습니다. 회원 탈퇴시 중요한건 회원 정보를 삭제하는 것이라고 생각해서 이를 비동기적으로 처리하는게 옳다고 생각해서 이렇게 구현했는데, 이 방식이 옳은 방식인지 확신이 서질 않습니다. 현업에서는 어떻게 이런 문제를 해결하고 있는지 궁금합니다! 읽어주셔서 감사합니다!
-
해결됨스프링 부트 웹 개발 입문 - 따라하며 배우기
고객정보 실행에서 화면 안나옵니다..
와 어김없이 오류가...고민고미하다 오려봅니다.https://drive.google.com/drive/folders/1fI2ZNj1l2IrpzKHGAh2xNhPuPDsmZ3Mq
-
미해결스프링 부트 - 핵심 원리와 활용
라이브러리 만들 때 클래스를 감추는 방법 질문
라이브러리를 생성하는데 궁금한 점이 있습니다.라이브러리 안에는 A 클래스, B 클래스가 존재하고 여기서 외부로는 B 클래스만을 밖으로 내보내고 싶다고 할때는 어떤식으로 해야할까요?즉 A 클래스는 라이브러리 내부에서만 사용하도록 하고 B 클래스를 통해 외부에서 사용가능토록 하는 것입니다. default 접근 제어자를 생각해보았으나 라이브러리에 패키지가 여러 개일 경우 개발할 때 불편함이 있다고 생각되었습니다.항상 좋은 강의 감사드립니다 :)
-
미해결스프링 핵심 원리 - 기본편
Member 클래스에서 Grade 오류
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]여기에 질문 내용을 남겨주세요. 안녕하세요! 사진 첨부와 같이 Grade에서 빨간줄 에러가 뜨는데 왜그럴까요..?ㅜㅜ 영한쌤 카테고리를 보면 member 패키지안에 Grade Enum 클래스가 있는거 같은데 제꺼엔 그클래스가 첨에 파일 불러오기 할때부터 없던데 혹시 그래서일까요..?
-
미해결스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
test.mv.db 삭제 방법
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]여기에 질문 내용을 남겨주세요. 버전을 최신 버전으로 다운받아서 강의자료에 있는 1.4.00 버전으로 다시 다운을 받았는데요, test.mv.db 파일을 삭제해야 한다고 하셔서 해보는데 윈도우라 어떻게 하는건지 모르겠습니다...ㅠㅠdir /w로 검색해서이렇게는 나왔는데 rm test.mv.db 또는 윈도우는 rmdir test.mv.db로 지우는게 아닌가요?이렇게 나오고 삭제가 되지 않는데 삭제 방법이 궁금합니다ㅠ
-
해결됨스프링 MVC 2편 - 백엔드 웹 개발 활용 기술
생성한 변수 hm 미사용 & 미치는 영향
스프링 MVC 2편 중 섹션 7. 로그인 처리2 - 필터, 인터셉터의 '스프링 인터셉터 - 요청 로그' 강의 11:09쯤에 코드 중 다음과 같은 코드가 있는데요. if(handler instanceof HandlerMethod){HandlerMethod hm = (HandlerMethod) handler;}위와 같이 만든 hm이라는 변수를 사용하지 않는데 만드는 이유가 뭔지, 왜 캐스팅해서 무엇을 보여주고자 하는지 모르겠습니다.바로 밑에 로그에 출력하는 handler에 영향을 주나요?
-
해결됨스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
ConflictingBeanDefinitionException
질문:ConflictingBeanDefinitionException 을 왜 import 할 수 없는건가요? 사례:자동 빈 등록 vs 자동 빈 등록에 대해서 아래와 같이 테스트 코드를 작성해서ConflictingBeanDefinitionException 예외를 발생시키는지 검증하는 테스트 코드를 작성해보았습니다그런데 아래와 같이 ConflictingBeanDefinitionException 을 찾을 수 없다고 오류가 발생하였습니다.자동 import가 안되길래 검색을 통해서 경로를 찾아서 직접 아래와 같이 임포트 하였습니다.그런데 여전히 import가 되지를 않는데 어떤 문제가 있나요?
-
미해결토비의 스프링 부트 - 이해와 원리
블로그 정리 문의
안녕하세요 토비님먼저 좋은 강의 만들어주셔서 감사드립니다.스프링의 기본부터 다시 배우고자 해당 강의를 신청하게 됐습니다. 다름이 아니라 혹시 강의 내용과 pdf 강의 자료를 제 블로그에 정리해서올려도 되는지 여쭙고자 글 남겨드렸습니다.다른 분들과 공유보다는 스스로 복습하는 용도로 글을 작성하는 목적이 크지만 인터넷에 올라오는 내용인 만큼 먼저 허락을 구하고자 합니다.(강의 내용과 강의 자료를 그대로 복붙하지는 않고 스스로 정리한 다음 포스팅 하고자 합니다.)최대한 지양하겠지만 필요하다면 pdf 파일 속 그림이나 사진을 올릴때도 있을것 같습니다.당연히 출처는 남기겠습니다.만약 불가하시다면 비공개 처리하여 공유 및 열람이 안 되도록 조치해놓겠습니다.
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
@NotEmpty적용 안되는 오류.
1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 아니오[질문 내용]'회원 등록' 강의 듣던 중 오류입니다.회원가입 시 이름이 공백이면 @NotEmpty로 빠꾸시키는 코드 작성한 상태인데, 공백 그대로 db에 저장되고 있습니다. db저장된 후 회원가입을 다시 진행했을 경우에는 기존의 공백을 id로 인식해 "이미 존재하는 회원입니다."라며 500에러가 뜨고 있는 상태이고요.구글링 해보고 chatGPT까지 썼음에도 아직까지 못잡고 있습니다... 도움 부탁드려요.코드 일부 보내드립니다.Controller@Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; //코드 일부 생략 @PostMapping("/members/new") public String create(@Valid MemberForm form, BindingResult result) { if (result.hasErrors()) { return "members/createMemberForm"; } Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode()); Member member = new Member(); member.setName(form.getName()); member.setAddress(address); memberService.join(member); return "redirect:/"; } MemberForm@Getter @Setter public class MemberForm { @NotEmpty(message = "회원명은 필수입니다.") private String name; private String city; private String street; private String zipcode; } createMemberForm.html<div th:replace="fragments/bodyHeader :: bodyHeader"/> <form role="form" action="/members/new" th:object="${memberForm}" method="post"> <div class="form-group"> <label th:for="name">이름</label> <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'"> <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">정확하지 않은 답변입니다.</p>어느 부분이 문제인지 알 수 있을까요?
-
미해결실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
cacade에 관하여
왜 cascade를 order 엔티티에만 안되는지 궁금합니다. 다른 엔티티들도 매핑이 되있을텐데 (item이라던지) 왜 order 에만 cascade 옵션을 건건가요???
-
미해결스프링 핵심 원리 - 기본편
스프링 컨테이너가 등록하는 것
질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]강의에서 스프링 컨테이너에서 뭐를 등록한다고 하는데 정확히 등록하는게 뭔가요??메소드랑 객체같은걸 말하는 건가요??그리고 빈이라는거는 그냥 @Bean 어노테이션 붙은걸 빈이라고 하는게 맞나요??
-
미해결스프링 핵심 원리 - 기본편
컴포넌트 스캔 강의 @Component 붙이는 곳
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]컴포넌트 스캔 강의에서 모든 클래스에 @Component 어노테이션을 붙이지 않는데요 예를 들면 Order 클래스에는 @Component 를 붙이지 않는데모든 클래스에는 왜 안붙이고 특정 클래스에만 붙이는 건가요?그리고 어떤 클래스에 붙여야 하는건가요??
-
미해결스프링 핵심 원리 - 기본편
컴포넌트 스캔 강의와 관련해 @Autowired 사용과 의존관계 주입
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]컴포넌트 스캔 강의를 듣다가 의존관계를 주입한다고 @Autowired 어노테이션을 붙이는데 생성자에만 붙이는건가요?? 정확히 의존관계 주입이라는 개념이 뭔지를 잘 모르겠어서요ㅠㅠㅠㅠ의존관계라는게 정확히 뭘 말하는 건가요?그리고 @Autowired 어노테이션은 어디에다가 붙이는 건가요??
-
미해결자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
war 파일로 tomcat 배포 방법 문의?
jar 파일이 아닌 war 파일로 빌드하여, tomcat에 배포하고 싶었습니다.tomcat 배포 방법을 알려주시면 감사하겠습니다.사실 검색하여 해보았으나, 이상하게 404 에러가 계속 나오고 있습니다. ㅜㅜintellj에서는 문제 없고, jar 파일 생성후, 실행해도 문제는 없는데, war파일을 tomcat 배포시 문제가 있습니다.강사님의 많은 경험으로, 답변 주시면 감사하겠습니다.
-
미해결스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
500오류 - NullPointerException
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예/아니오)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오)[질문 내용]여기에 질문 내용을 남겨주세요. 전송 버튼을 누르면이런 화면이 나옵니다!에러는 NullPointerException이 뜨고근데 f12를 눌러 form 태그로 전송된 데이터는 조회가 됩니다어디가 문제인 걸까요 ㅜㅜ