묻고 답해요
160만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
redis 사용하는데 ConnectionTimeoutError: Connection timeout이라는 오류가 자꾸 발생합니다.
안녕하세요 강사님강사님 영상을 통해 학습하고 자그마한 프로젝트를 수행하고 있는데 서버 실행은 되지만 아래와 같은 오류가 발생합니다.app.js의 코드는 아래와 같습니다.강의랑 ppt 교안에는 const RedisStore = require('connect-redis')(sesson); 으로 되어있으나저같은 경우에 아래와 같은 오류가 발생해서 require('connect-redis').default;로 변경했었습니다.
-
미해결[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
강의 소리는 작은데 엔딩음악은 너무 커서 귀청 떨어지는줄 알았네요.
무료강좌 중에 이런게 많아요. 다음 강의 제작때는 소리를 맞추든가 음악을 안넣든가 하는게 좋을것 같애요.
-
미해결[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
어렵습니다...
전체적으로 어렵고 잘 이해가 되지 않습니다.. 지금 9장 거의 끝나가는데 계속 가는게 맞을까요..? 아니면 복습을 다시 하는게 맞는건가요.. 혼자하려니 많이 어렵네요 조언 부탁드립니다.
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
섹션3.http 모듈로 서버 만들기 관련 질문입니다.
섹션3.http 모듈로 서버 만들기 관련 질문입니다.코드를 보면, 클라이언트가 서버에 요청하는 get()함수를 사용할 때get("/users"), 즉 users라는 url을 클라이언트 측에 전달하도록 설정했는데,클라이언트가 서버에 데이터를 신규로 등록하거나, 수정, 삭제를요청할 때에는 post("/user", {name})이렇게 작성하는 이유가 뭘까요?그냥 생각하기에 클라이언트가 요청할 때, 서버측에서 전달할 페이지 url이/users 이니까, 그냥 수정 및 삭제도 동일한 /users 주소에서 하면 안되는건지 궁금해서요. 그리고, 두 번째 질문입니다. 만약 위 질문이 애초에 불가능한 것이라고 한다면서버에서는 /user에 수정. 삭제된 데이터 값을 어떻게 /users에 전달해서 새롭게변경된 값으로 업데이트하는 것인지 궁금합니다.(*서버에 대한 개념부족으로 인해 문의드립니다.)
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
파람스에 인코딩해서 보낼 때 질문
파람스부분을 인코딩해서 보냈는데 API서버에선 디코딩 안하고 바로 req.params.title를 꺼내 썼잖아요. 콘솔로 찍어보니깐 자동으로 디코딩 되어있던데 이건 익스프레스에서 알아서 디코딩 한 것인가요?
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
에러처리 질문
6:25에서 POST /v1/token 요청시 성공인 200응답이 아니면 401응답으로 등록되지 않은 도메인이었거나 500응답으로 기타 에러였는데 그런 응답을 받았다면 else문 건너뛰고 바로 catch문으로 이동하게 되는 것 아닌가요? 6:25에 작성한 else문은 어떤 상황에 작동되는 건지 궁금합니다.
-
미해결[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
시퀄라이즈안쓰고 mysql연결하려고 합니다.
기존에 mysql에 연결은 이런식으로 했습니다. 코드가 너무 지져분해서 mvc패턴으로 나눠보려고 하는중입니다.아래처럼 바꾸고 db연결하고 로그인하려고 하는데 이렇게 오류가 발생하고있습니다.구글링해봤는데 대부분은 처음에했던 위에것처럼 많이 나오는거 같아요. db를 models폴더에 mysql.js에 정의하고 controller에서 연결해서 사용하고싶습니다.혹시 controller에서 쿼리문입력하면 너무 지저분한거 같아서 강사님이 models의 user.js에 시퀄라이즈를 이용해서 db에 정보넣는것처럼 쿼리문 따로 폴더를 만들어서 가져오고 사용할수있나요?(쿼리문을 밖으로 빼서 사용하고싶습니다.) 자세히 설명해 주시면 감사하겠습니다.
-
미해결[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
res.render 질문
사용자 이름을 누르면 사용자의 게시글이 출력되는 기능 구현중 res.render가 동작하지 않아 질문 드립니다.아래가 컨트롤러 부분이고 console.log(posts[0].User.nick); 이부분도 출력 되는걸 확인했습니다.그런데 title 안에 posts[0].User.nick 이 부분에서 오류가 뜨는데 위 console.log에서 출력이 되는데 왜 여기선 오류가 뜨는지도 모르겠고 title과 twits 부분을 지우고 main을 다른 페이지로 바꿨는데도 아예 동작을 하지 않는걸 확인했습니다. title 부분에서 오류가 안떠도 페이지가 제대로 로드 되지도 않습니다. exports.renderUpost = async (req, res, next) => { try { console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); let posts = []; if (req.params.userId) { const posts = await Post.findAll({ include: [ { model: User, where: { id: req.params.userId }, attributes: ["id", "nick"], }, ], }); console.log(posts[0].User.nick); } res.render("main", { title: ` ${posts[0].User.nick} | NodeBird`, twits: posts, }); } catch (error) { console.error(error); next(error); } }; 제가 작성자 이름을 a 태그로 해서 아래 코드처럼 수정해서 했는데 혹시 이 a태그에 넣은것들이 잘못된건가요? 위 코드에서 console.log 까지 출력 다 되는거 보면 문제가 없을것 같은데 그나마 생길것 같은게 이부분뿐이라 올려봅니다.<a href="#" onclick="findUpost(this);return false;" class="twit-author">{{twit.User.nick}}</a>
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
JWT 시그니처, 비밀키 부분 질문
1:49 책 내용을 보면 시그니처 자체는 숨기지 않아도 되고, 비밀 키만 숨기면 된다고 적혀있는데 시그니처 안에 비밀 키가 들어있으니 이것도 유출되면 안 되는게 아닌가요? 비밀 키를 찾지 않고 그냥 바로eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.he0ErCNloe4J7Id0Ry2SEDg09lKkZkfsRiGsdX_vgEg이런 JWT문자열을 가로채서 API서버로 보내면 변조는 아니지만 그냥 그 사람인 척 할 수 있는게 아닌가요?
-
해결됨Spring Boot JWT Tutorial
JWT에 생성할 때 질문
JwtProviderpackage com.example.project1.config.jwt; import com.example.project1.config.auth.PrincipalDetails; import com.example.project1.domain.jwt.TokenDTO; import com.example.project1.domain.member.UserType; 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.*; 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, List<GrantedAuthority> authorities) { // UsernamePasswordAuthenticationToken // [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]] // 여기서 Authenticated=false는 아직 정상임 // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정 // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며, // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다. log.info("authentication in JwtProvider : " + authentication); // userType in JwtProvider : ROLE_USER log.info("userType in JwtProvider : " + authorities); // 권한 가져오기 // authentication 객체에서 권한 정보(GrantedAuthority)를 가져와 문자열 형태로 변환한 후, // 쉼표로 구분하여 조인한 결과를 authorities 변수에 저장합니다. 따라서 authorities는 권한 정보를 문자열 형태로 가지게 됩니다. // 권한 정보를 문자열로 변환하여 클레임에 추가하는 방식 // String authorities = authentication.getAuthorities().stream() // .map(GrantedAuthority::getAuthority) // .collect(Collectors.joining(",")); Map<String, Object> claims = new HashMap<>(); claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); log.info("claims in JwtProvider : " + claims); log.info("authentication.getName() in JwtProvider : " + authentication.getName()); long now = (new Date()).getTime(); Date now2 = new Date(); // AccessToken 생성 Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() // 내용 sub : 유저의 이메일 // 토큰 제목 // JWT의 "sub" 클레임을 설정하는 메서드입니다. // "sub" 클레임은 일반적으로 사용자를 식별하는 용도로 사용되며, // 이메일과 같은 사용자의 고유한 식별자를 담고 있을 수 있습니다. .setSubject(authentication.getName()) .setIssuedAt(now2) // 클레임 id : 유저 ID // .claim(AUTHORITIES_KEY, authorities) .setClaims(claims) // 내용 exp : 토큰 만료 시간, 시간은 NumericDate 형식(예: 1480849143370)으로 하며 // 항상 현재 시간 이후로 설정합니다. .setExpiration(accessTokenExpire) // 서명 : 비밀값과 함께 해시값을 ES256 방식으로 암호화 .signWith(key, SignatureAlgorithm.HS256) .compact(); Claims claims2 = Jwts.parser().setSigningKey(key).parseClaimsJws(accessToken).getBody(); String subject = claims2.getSubject(); log.debug("claims subject 확인 in JwtProvider : " + subject); // Claims claim = Jwts.parserBuilder() // .setSigningKey(key) // .build() // .parseClaimsJws(accessToken).getBody(); // accessToken in JwtProvider : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6eHp6NDVAbmF2ZXIuY2 // 9tIiwiaWF0IjoxNjg5OTk1MzM3LCJhdXRoIjoiUk9MRV9VU0VSIiwiZXhwIjoxNjkzNTk1MzM3fQ.2_2PR-A // X9N0jKDyA7LpK7xRRBZBYZ17_f8Jq2TY4ny8 log.info("accessToken in JwtProvider : " + accessToken); // claim에서 auth 확인 in JwtProvider : ROLE_USER log.info("claim에서 accessToken에 담김 auth 확인 in JwtProvider : " + claims); // RefreshToken 생성 Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setSubject(authentication.getName()) .setClaims(claims) .setIssuedAt(now2) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("refreshToken in JwtProvider : " + refreshToken); log.info("claim에서 refreshToken에 담긴 auth 확인 in JwtProvider : " + claims); TokenDTO tokenDTO= TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .refreshToken(refreshToken) .accessTokenTime(accessTokenExpire) .refreshTokenTime(refreshTokenExpire) // principalDeatails에서 getUserName 메소드가 반환한 것을 담아준다. // 이메일을 반환하도록 구성했으니 이메일이 반환됩니다. .userEmail(authentication.getName()) .build(); log.info("tokenDTO in JwtProvider : " + tokenDTO); return tokenDTO; } // 소셜 로그인 성공시 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("auth", userDetails.getAuthorities()); log.info("claims : " + claims); // 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(); TokenDTO tokenDTO = TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .refreshToken(refreshToken) .userEmail(userDetails.getUsername()) .build(); log.info("tokenDTO in JwtProvider : " + tokenDTO); return tokenDTO; } // accessToken 생성 // 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성하는 로직을 구현 public TokenDTO createAccessToken(String userEmail, List<GrantedAuthority> authorities) { Long now = (new Date()).getTime(); Date now2 = new Date(); Date accessTokenExpire = new Date(now + this.accessTokenTime); log.info("authorities : " + authorities); Map<String, Object> claims = new HashMap<>(); claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); log.info("claims : " + claims); String accessToken = Jwts.builder() .setIssuedAt(now2) .setSubject(userEmail) .setExpiration(accessTokenExpire) .setClaims(claims) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("accessToken in JwtProvider : " + accessToken); // log.info("claim에서 accessToken에 담김 auth 확인 in JwtProvider : " + auth); TokenDTO tokenDTO = TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .userEmail(userEmail) .accessTokenTime(accessTokenExpire) .build(); log.info("tokenDTO in JwtProvider : " + tokenDTO); return tokenDTO; } // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 코드 // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴 // 인증 정보 조회 public Authentication getAuthentication(String token) { // 토큰 복호화 메소드 Claims claims = parseClaims(token); log.info("claims in JwtProvider : " + claims); if(claims.get("auth") == null) { throw new RuntimeException("권한 정보가 없는 토큰입니다."); } Object auth = claims.get("auth"); // [ROLE_USER] log.info("auth in JwtProvider : " + auth); // 클레임 권한 정보 가져오기 List<String> authorityStrings = (List<String>) claims.get(AUTHORITIES_KEY); // [ROLE_USER] log.info("authorityStrings in JwtProvider : " + authorityStrings); Collection<? extends GrantedAuthority> authorities = authorityStrings.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // [ROLE_USER] log.info("authorities in JwtProvider : " + authorities); // UserDetails 객체를 만들어서 Authentication 리턴 // User principal = new User(claims.getSubject(), "", authorities); // log.info("principal in JwtProvider : " + principal); log.info("claims.getSubject() in JwtProvider : " + claims.getSubject()); return new UsernamePasswordAuthenticationToken(claims.getSubject(), 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; import java.net.URI; // 클라이언트 요청 시 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); // jwt : // eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6eHp6NDVAbmF2ZXIuY29tIiwiaWF // 0IjoxNjg5OTQ0OTk0LCJhdXRoIjoiIiwiZXhwIjoxNjg5OTQ1MzU0fQ.qyR2bJMDmNb1iv // q6a4W55dGBmyFEzaENN1-F7qPlJKw log.info("jwt : " + jwt); String requestURI = httpServletRequest.getRequestURI(); // requestURI/api/v1/users/1 log.info("requestURI" + requestURI); if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)){ // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장 Authentication authentication = jwtProvider.getAuthentication(jwt); // UsernamePasswordAuthenticationToken // [Principal=null, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]] log.info("authentication in JwtAuthenticationFilter : " + authentication); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("Security Context에 인증 정보를 저장했습니다. 정보 : {}",authentication.getName()); } 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; } } }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 in PrincipalDetailsService : " + member); return new PrincipalDetails(member); } } PrincipalDetailspackage com.example.project1.config.auth; import com.example.project1.entity.member.MemberEntity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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 @NoArgsConstructor @Slf4j public class PrincipalDetails implements UserDetails, OAuth2User { // 일반 로그인 정보를 저장하기 위한 필드 private MemberEntity member; // OAuth2 로그인 정보를 저장하기 위한 필드 // attributes는 Spring Security에서 OAuth2 인증을 수행한 후에 사용자에 대한 추가 정보를 저장하는 데 사용되는 맵(Map)입니다. // OAuth2 인증은 사용자 인증 후에 액세스 토큰(Access Token)을 발급받게 되는데, // 이 토큰을 사용하여 OAuth2 서비스(provider)로부터 사용자의 프로필 정보를 요청할 수 있습니다. // 예를 들어, 소셜 로그인을 사용한 경우에는 attributes에는 사용자의 소셜 서비스(provider)에서 제공하는 프로필 정보가 담겨 있습니다. // 소셜 로그인 서비스(provider)마다 제공하는 프로필 정보가 다를 수 있습니다. // 일반적으로 attributes에는 사용자의 아이디(ID), 이름, 이메일 주소, 프로필 사진 URL 등의 정보가 포함됩니다. /* * 구글의 경우 * { "sub": "100882758450498962866", // 구글에서 발급하는 고유 사용자 ID "name": "John Doe", // 사용자 이름 "given_name": "John", // 이름(이름 부분) "family_name": "Doe", // 성(성(성) 부분) "picture": "https://lh3.googleusercontent.com/a/AAcHTtdzQomNwZCruCcM0Eurcf8hAgBHcgwvbXEBQdw3olPkSg=s96-c", // 프로필 사진 URL "email": "johndoe@example.com", // 이메일 주소 "email_verified": true, // 이메일 주소 인증 여부 "locale": "en" // 지역 설정 } * */ 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())); log.info("collection : " + collection); 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 // OAuth2 인증에서는 사용되지 않는 메서드이므로 null 반환 public String getName() { return null; } }controller // 로그인 @PostMapping("/api/v1/users/login") public ResponseEntity<?> login(@RequestBody MemberDTO memberDTO) throws Exception { log.info("member : " + memberDTO); try { log.info("-----------------"); ResponseEntity<TokenDTO> login = memberService.login(memberDTO.getUserEmail(), memberDTO.getUserPw()); log.info("login : " + login); return ResponseEntity.ok().body(login); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); } } // refresh로 access 토큰 재발급 // @RequsetHeader"Authorization")은 Authorization 헤더에서 값을 추출합니다. // 일반적으로 리프레시 토큰은 Authorization 헤더의 값으로 전달되며, // Bearer <token> 형식을 따르는 경우가 일반적입니다. 여기서 <token> 부분이 실제 리프레시 토큰입니다 // 로 추출하면 다음과 같이 문자열로 나온다. // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String token) throws Exception { try { ResponseEntity<TokenDTO> accessToken = refreshTokenService.createAccessToken(token); return ResponseEntity.ok().body(accessToken); } catch (Exception e) { throw new RuntimeException(e); } }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.entity.member.embedded.AddressEntity; 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.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; 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 "이미 가입된 회원입니다."; } else { // 아이디가 없다면 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()) .address(AddressEntity.builder() .userAddr(memberDTO.getAddressDTO().getUserAddr()) .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail()) .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc()) .build()) .build(); log.info("member : " + member); 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 String remove(Long userId) { MemberEntity member = memberRepository.deleteByUserId(userId); if(member == null) { return "회원 탈퇴 완료!"; } else { return "회원 탈퇴 실패!"; } } // 로그인 public ResponseEntity<TokenDTO> login(String userEmail, String userPw) throws Exception { MemberEntity findUser = memberRepository.findByUserEmail(userEmail); log.info("findUser : " + findUser); if (findUser != null) { // 사용자가 입력한 패스워드를 암호화하여 사용자 정보와 비교 if (passwordEncoder.matches(userPw, findUser.getUserPw())) { // UsernamePasswordAuthenticationToken은 Spring Security에서 // 사용자의 이메일과 비밀번호를 이용하여 인증을 진행하기 위해 제공되는 클래스 // 이후에는 생성된 authentication 객체를 AuthenticationManager를 이용하여 인증을 진행합니다. // AuthenticationManager는 인증을 담당하는 Spring Security의 중요한 인터페이스로, 실제로 사용자의 인증 과정을 처리합니다. // AuthenticationManager를 사용하여 사용자가 입력한 이메일과 비밀번호가 올바른지 검증하고, // 인증에 성공하면 해당 사용자에 대한 Authentication 객체를 반환합니다. 인증에 실패하면 예외를 발생시킵니다. // 인증은 토큰을 서버로 전달하고, 서버에서 해당 토큰을 검증하여 사용자를 인증하는 단계에서 이루어집니다. Authentication authentication = new UsernamePasswordAuthenticationToken(userEmail, userPw); // UsernamePasswordAuthenticationToken // [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]] // 여기서 Authenticated=false는 아직 정상임 // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정 // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며, // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다. log.info("authentication in MemberService : " + authentication); List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(findUser); // TokenDTO token = jwtProvider.createToken(authentication, findUser.getUserType()); TokenDTO token = jwtProvider.createToken(authentication, authoritiesForUser); log.info("tokenEmail in MemberService : "+ token.getUserEmail()); TokenEntity checkEmail = tokenRepository.findByUserEmail(token.getUserEmail()); log.info("checkEmail in MemberService : " + checkEmail); // 사용자에게 이미 토큰이 할당되어 있는지 확인합니다. if (checkEmail != null) { log.info("이미 발급한 토큰이 있습니다."); // // 기존 토큰을 업데이트할 때 사용할 임시 객체로 TokenDTO token2를 생성합니다. TokenDTO token2 = TokenDTO.builder() .id(checkEmail.getId()) .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(token.getUserEmail()) .nickName(findUser.getNickName()) .userId(findUser.getUserId()) .accessTokenTime(token.getAccessTokenTime()) .refreshTokenTime(token.getRefreshTokenTime()) .userType(findUser.getUserType()) .build(); // 기존 토큰을 업데이트할 때 사용할 임시 객체로 TokenEntity tokenEntity2를 생성합니다. TokenEntity updateToken = TokenEntity.builder() .id(token2.getId()) .grantType(token2.getGrantType()) .accessToken(token2.getAccessToken()) .refreshToken(token2.getRefreshToken()) .userEmail(token2.getUserEmail()) .nickName(token2.getNickName()) .userId(token2.getUserId()) .accessTokenTime(token2.getAccessTokenTime()) .refreshTokenTime(token2.getRefreshTokenTime()) .userType(token2.getUserType()) .build(); log.info("token in MemberService : " + updateToken); tokenRepository.save(updateToken); } else { log.info("발급한 토큰이 없습니다."); token = TokenDTO.builder() .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(token.getUserEmail()) .nickName(findUser.getNickName()) .userId(findUser.getUserId()) .accessTokenTime(token.getAccessTokenTime()) .refreshTokenTime(token.getRefreshTokenTime()) .userType(findUser.getUserType()) .build(); // 새로운 토큰을 DB에 저장할 때 사용할 임시 객체로 TokenEntity tokenEntity를 생성합니다. TokenEntity newToken = TokenEntity.builder() .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(token.getUserEmail()) .nickName(token.getNickName()) .userId(token.getUserId()) .accessTokenTime(token.getAccessTokenTime()) .refreshTokenTime(token.getRefreshTokenTime()) .userType(token.getUserType()) .build(); log.info("token in MemberService : " + newToken); tokenRepository.save(newToken); } HttpHeaders headers = new HttpHeaders(); // response header에 jwt token을 넣어줌 headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + token); return new ResponseEntity<>(token, headers, HttpStatus.OK); } } else { return null; } return null; } private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) { // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현 // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다. UserType role = member.getUserType(); // 사용자의 권한 정보를 가져오는 로직 (예시) List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" +role.name())); log.info("role in MemberService : " + role.name()); log.info("authorities in MemberService : " + authorities); return authorities; } // 회원정보 수정 public MemberDTO update(MemberDTO memberDTO) { MemberEntity findUser = memberRepository.findByUserEmail(memberDTO.getUserEmail()); if(findUser != null) { findUser = MemberEntity.builder() .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userType(memberDTO.getUserType()) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .address(AddressEntity.builder() .userAddr(memberDTO.getAddressDTO().getUserAddr()) .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail()) .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc()) .build()).build(); memberRepository.save(findUser); MemberDTO modifyUser = MemberDTO.toMemberDTO(Optional.of(findUser)); return modifyUser; } else { MemberEntity member = MemberEntity.builder() .userEmail(memberDTO.getUserEmail()) .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .userType(memberDTO.getUserType()) .address(AddressEntity.builder() .userAddr(memberDTO.getAddressDTO().getUserAddr()) .userAddrDetail(memberDTO.getAddressDTO().getUserAddrDetail()) .userAddrEtc(memberDTO.getAddressDTO().getUserAddrEtc()) .build()) .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 in MemberService : " + userEmail); MemberEntity findMember = memberRepository.findByUserEmail(userEmail); // 권한 정보 추출 List<GrantedAuthority> authorities = getAuthoritiesForUser(findMember); // UserDetails 객체 생성 (사용자의 아이디 정보를 활용) // 첫 번째 인자 : username 사용자 아이디 // 두 번째 인자 : 사용자의 비밀번호 // 세 번째 인자 : 사용자의 권한 정보를 담은 컬렉션 UserDetails userDetails = new User(userEmail, null, authorities); log.info("userDetails in MemberService : " + userDetails); TokenDTO token = jwtProvider.createToken2(userDetails); log.info("token in MemberService : " + token); return ResponseEntity.ok().body(token); } }RefreshTokenServicepackage com.example.project1.service.jwt; 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.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.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service @RequiredArgsConstructor @Slf4j public class RefreshTokenService { private final TokenRepository tokenRepository; private final JwtProvider jwtProvider; private final MemberRepository memberRepository; public ResponseEntity<TokenDTO> createAccessToken(String refreshToken) { // refreshToken 유효성 검사하고 true면 넘어감 if(jwtProvider.validateToken(refreshToken)) { TokenEntity findRefreshTokenEmail = tokenRepository.findByRefreshToken(refreshToken); // 아이디 추출 String userEmail = findRefreshTokenEmail.getUserEmail(); log.info("userEmail : " + userEmail); MemberEntity member = memberRepository.findByUserEmail(userEmail); log.info("member : " + member); // 사용자의 권한 정보를 가져옴 List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(member); TokenDTO accessToken = jwtProvider.createAccessToken(userEmail, authoritiesForUser); log.info("accessToken : " + accessToken); accessToken = TokenDTO.builder() .grantType(accessToken.getGrantType()) .accessToken(accessToken.getAccessToken()) .userEmail(accessToken.getUserEmail()) .nickName(member.getNickName()) .userId(member.getUserId()) .accessTokenTime(accessToken.getAccessTokenTime()) .build(); TokenEntity tokenEntity = TokenEntity.builder() .grantType(accessToken.getGrantType()) .accessToken(accessToken.getAccessToken()) .userEmail(accessToken.getUserEmail()) .nickName(accessToken.getNickName()) .userId(accessToken.getUserId()) .accessTokenTime(accessToken.getAccessTokenTime()) .build(); log.info("token : " + tokenEntity); tokenRepository.save(tokenEntity); HttpHeaders headers = new HttpHeaders(); // response header에 jwt token을 넣어줌 headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + accessToken); return new ResponseEntity<>(accessToken, headers, HttpStatus.OK); } else { throw new IllegalArgumentException("Unexpected token"); } } // 주어진 사용자에 대한 권한 정보를 가져오는 로직을 구현하는 메서드입니다. // 이 메서드는 데이터베이스나 다른 저장소에서 사용자의 권한 정보를 조회하고, // 해당 권한 정보를 List<GrantedAuthority> 형태로 반환합니다. private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) { // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현 // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다. UserType role = member.getUserType(); // 사용자의 권한 정보를 가져오는 로직 (예시) log.info("role : " + role.name()); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + role.name())); return authorities; } }이런식으로 코드를 작성했습니다. 로그인 시 JWT는 제대로 생성해주는 것을 볼 수 있고 권한이 주어지고 auth에 ROLE_USER이런식으로 들어가는 것을 확인했습니다. 근데 .setSubject(authentication.getName())를 넣었는데 헤더에 토큰을 담아서 보내는 테스트를 할 때 public Authentication getAuthentication(String token) { // 토큰 복호화 메소드 Claims claims = parseClaims(token); log.info("claims in JwtProvider : " + claims); if(claims.get("auth") == null) { throw new RuntimeException("권한 정보가 없는 토큰입니다."); } Object auth = claims.get("auth"); // [ROLE_USER] log.info("auth in JwtProvider : " + auth); // 클레임 권한 정보 가져오기 List<String> authorityStrings = (List<String>) claims.get(AUTHORITIES_KEY); // [ROLE_USER] log.info("authorityStrings in JwtProvider : " + authorityStrings); Collection<? extends GrantedAuthority> authorities = authorityStrings.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // [ROLE_USER] log.info("authorities in JwtProvider : " + authorities); // UserDetails 객체를 만들어서 Authentication 리턴 // User principal = new User(claims.getSubject(), "", authorities); // log.info("principal in JwtProvider : " + principal); log.info("claims.getSubject() in JwtProvider : " + claims.getSubject()); return new UsernamePasswordAuthenticationToken(claims.getSubject(), 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(); } }auth는 제대로 나오는데 claims.getSubject() 이 부분이 null이 나옵니다. 즉 권한을 제대로 주어지는데 new UsernamePasswordAuthenticationToken(claims.getSubject(), token, authorities); 인증하는 이부분에서 인자가 (이메일, 토큰, 권한)이렇게 주어져야 하는데 (null, 토큰, 권한) 이렇게 갑니다. JwtAuthenticationFilter에서 Authentication authentication = jwtProvider.getAuthentication(jwt); authentication 담아주는 것을 로그로 찍어본 결과 // [Principal=null, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]] 이렇게 로그가 찍혔습니다. 로그를 보면 인증은 true니까 된거 같은데 log.info("Security Context에 인증 정보를 저장했습니다. 정보 : {}", authentication.getName()); 여기서도는 null도 아니고 앞에 문자열만 뜨고 안뜹니다. 근데 JwtProvider에서 log로 찍어보면 이메일이 제대로 나오고 있습니다....
-
미해결[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
좋아요 기능 구현중 에러 질문입니다.
첫번째 부터 순서대로post모델,user모델 passport.index의 deserializeUser부분 route.page 부분 입니다.User is associated to User multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include. 위 같은 오류가 뜨는데 어떤게 문제인지 아무리 찾아봐도 모르겠습니다..
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
include의 as옵션 질문
include의 as옵션으로 넣은 가명이 belongsToMany의 as옵션으로 설정한 가명을 찾는 것인가요?chatGPT 는 include의 as옵션이 그냥 찾은 객체에 가명을 붙이는 것 뿐이라고 하던데 include: [ { model: User, attributes: ['id', 'nick'], as: 'Followers', }, { model: User, attributes: ['id', 'nick'], as: 'Followings', }, ] })여기서 model옵션으로 똑같이 User를 넣었는데 두 개의 결괏값이 다르게 나온다는 게 이해가 안 돼요
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
좋아요 기능 구현중 시퀄라이즈 add함수 질문
3번째 사진의 post.addliked(parseInt~~ 이 부분에서 addliked 라는 함수가 존재하지 않는다고 하는데 s를 붙여봐도 as에 해당하는 부분으로 교체해봐도 전부 오류가 뜨네요왜그럴까요....
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
GET /cart.json 404 4.349 ms - 2902 에러
게시글 업로드시 다른 요청/응답은 다 정상이고 게시글 업로드와 DB생성도 다 정상적으로 작동되던데 마지막에 GET /cart.json 404 4.349 ms - 2902 에러가 뜨던데 이번 코드에서 /cart.json 요청을 한 코드가 있었나요? 신경을 안 써도 되는 부분인지 궁금합니다.
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
시퀄라이즈 실습 강의 : CREATE 가 아니라 SELECT가 출력됩니다.
-C:\Users\ysm65\learn-sequelize>npm start> learn-seuqellize@0.0.1 start> nodemon app[nodemon] 3.0.1[nodemon] to restart at any time, enter rs[nodemon] watching path(s): .[nodemon] watching extensions: js,mjs,cjs,json[nodemon] starting node app.js3001 번 포트에서 대기중Executing (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'users' AND TABLE_SCHEMA = 'nodejs'Executing (default): SHOW INDEX FROM users FROM nodejsExecuting (default): SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'comments' AND TABLE_SCHEMA = 'nodejs'Executing (default): SHOW INDEX FROM comments FROM nodejs데이터베이스 연결 성공 --개정 3판 7장 내용 실습 중 마지막에 브라우저 창에 띄우는 것 까지 확인을 했지만 이름을 클릭 시 comment 내용이 뜨지 않아 하나씩 확인하면서 오류를 찾고 있습니다. 그 과정에서 3판 교과서 344page에서 소개된 내용과 다른 점을 찾았고 강사님과 다르게 CREATED가 아니라 SELECTED 로 뜨는 게 문제의 원인이지 않을까 생각하고 있습니다. 코드의 오타는 확인한 상황인데, 다른 원인이 어떤 게 있는지 고민을 하다가 이렇게 질문 글을 남깁니다. 어떻게 해결을 하는 게 좋을 지 조언을 구하기 위해서 질문을 남깁니다.
-
미해결스프링부트 시큐리티 & JWT 강의
마지막 강의에서doFilterInternal jwtHeader 테스트
package com.cos.jwt.config.jwt; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; // 시큐리티가 filter를 가지고 있는데, 그 필터중에 BasicAuthenticationFilter라는 것이 있음. // 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어있음. // 만약 권한이나 인증이 필요한 주소가 아니라면 이 필터를 안타요. public class JwtAuthorizationFilter extends BasicAuthenticationFilter { public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } //인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 타게 됨. @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { super.doFilterInternal(request, response, chain); System.out.println("인증이나 권한이 필요한 주소 요청이 됨."); String jwtHeader = request.getHeader("Authorization"); System.out.println("jwtHeader = " + jwtHeader); } }마지막 강의에서 해당 코드 작성하고, jwtHeader test해보려는데,저는 강사님과 다르게 GET요청보내면 콘솔에 "필터3"만 떠요!코드를 살펴보니, MyFilter3에 POST요청일 때만 돌아가도록 로직이 구현되었는데,제가 수업 중 놓친 걸까요? 작성된 코드로는 불가능한 결과물인 것 같아서요 ㅠ
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
req.session 질문
16:19의 3번에서 세션 객체를 찾아서 req.session으로 만든다고 했었는데 이전에 처음에 로그인 했을 때 req.session으로 등록된 세션 객체(13:40의 6번)는 사라진 건가요?
-
미해결스프링부트 시큐리티 & 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이 뜹니다.... 어떻게 해야할까요
-
해결됨스프링부트 시큐리티 & JWT 강의
Jwt 토큰 검증해서 정상적인 사용자인지 확인
13분 정도에 if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) { chain.doFilter(request, response); // 다시 필터를 타게 하는 것이다. return; } 위 코드를 작성하시는데 강의에서는 정상적인 사용자가 아닐 경우 다시 필터를 타게 한다고 하시는데질문1. 다시 필터를 타게 하는 것이 아니라 로그아웃을 시키는 것이 정상적인 방법이지 않을까요? (이유는 다시 필터를 타도 이미 정상적인 사용자가 아닌데 다시 필터를 타게 할 이유가 없다고 생각합니다.) 질문2. 스스로 학습하다가 든 생각인데 클라이언트가 로그인 시도를 하면서 서버로 넘긴 데이터이니까 JwtAuthenticationFilter에서 ObjectMapper om = new ObjectMapper(); User user = om.readValue(request.getInputStream(), User.class); System.out.println(user);이렇게 entity 객체로 받는데 dto 객체로 받는 것이 안전하지 않은가요? (물론 강의에서 dto class를 만들지는 않았지만) 질문3. JwtAuthorizationFilter에서Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);강제로 authentication을 만들어서 security session영역에 authentication을 집어넣는 이유가 궁금합니다.
-
미해결Spring Boot JWT Tutorial
AbstractHttpConfigurer, SecurityConfigurerAdapter 구분
먼저, 올려주신 소스와 강의 잘 봤습니다.감사합니다~!https://github.com/SilverNine/spring-boot-jwt-tutorial/blob/master/src/main/java/me/silvernine/tutorial/jwt/JwtSecurityConfig.java Spring Security Docs에서는 customFilter를 만들때의 예시를 AbstractHttpConfigurer로 하고 있습니다.그런데 작성하신 JwtSecurityConfig을 보면, extends를 SecurityConfigurerAdapter로 하셨는데 어떠한 이유가 있는지 궁금합니다.감사합니다.