묻고 답해요
169만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결
백엔드 웹 인증(세션/토큰/OAuth 등) 경험자분께 짧은 인터뷰 부탁드립니다 (5~10분)
학교에서 "웹서비스 인증 방식에 따른 보안성과 사용자 편의성 비교 분석"이라는 주제로 심화 탐구를 진행하고 있습니다!주제와 관련된 백엔드 인증 방식(세션 / JWT / OAuth 등)에 대한 이론적 비교와 원리 분석은 이미 정리한 상태입니다.다만 책이나 인터넷 자료만으로는 실제 프로젝트에서 인증 방식을 선택하는 기준이나 운영 과정에서의 경험, 그리고 이론과 실무 사이의 차이를 파악하기 어렵다고 판단하여 실무 경험자분들의 의견을 듣고자 합니다!질문 내용은 아래와 같습니다.1. 실제 프로젝트에서 인증 방식 선택 시 가장 중요하게 보는 기준은 무엇인가요?2. 세션 / JWT / OAuth 중 실무에서 주로 선택되는 방식과 그 이유는 무엇인가요?3. 인증 구현 및 운영 과정에서 가장 자주 겪는 문제는 무엇인가요?4. 이론과 실무에서 가장 크게 느낀 차이가 있다면 무엇인가요?5. 인증 시스템에서 보안성과 사용자 편의성이 충돌할 때, 실제 프로젝트에서는 어떤 기준으로 균형을 잡으셨나요?짧게라도 답변 주시면 과제에 정말 큰 도움이 됩니다. 감사합니다!
-
미해결AI 시대에 살아남기: Supabase로 백엔드 뚝딱!
수파베이스 ORM 질문
안녕하세요.수파베이스 ORM 섹션을 듣고나니 일반적인 백엔드 서버와 api 라우트를 정의하고 데이터를 주고받는게 아닌 클라이언트에서 직접 수파베이스 ORM를 호출할 수 있는 것으로 확인되는데요!현재 회사에 프론트엔드 개발자 2명이고 백엔드 지식이 많지않아 next.js + 수파베이스로 MVP 검증부터 진행할 것같습니다.수파베이스로 MVP 검증후 별도의 백엔드 서버로 분리하게되었을때(분리가 꼭 필요한지도 사실 잘 모르겠습니다.) 분리비용을 최소화히기위해 api호출 과정을 next.js의 api라우트를 무조건 거쳐서 수파베이스를 접근하게하려고하는데 이런 과정이 유의미한 작업이 될 수 있는지 궁금합니다.
-
미해결AI 시대에 살아남기: Supabase로 백엔드 뚝딱!
트리거 질문
안녕하세요. 프론트엔드만하다가 강의보고 백엔드에대해 알아가는중입니다. 백엔드쪽은 아예 기반이 없다보니 하나하나 해보면서 따라가고있는중입니다. 1. 트리거는 ui로 설정을안하고 SQL 에디터로 설정을 해주셨는데 따로 이유가 있을까요??2. 트리거와 그 안에서 작성해주신 업데이트 함수가 별도로 관리되는것같더라고요.functions가 유틸함수고 Triggers가 시점과 적용대상(어느 테이블에 적용할지?)를 관리하는 영역으로 이해하는게 맞을까요?3. 트리거는 테이블영역에서 적용이 불가능한가요? RSL처럼요!! 뭔가 분산되어있는 느낌이 있어서요. 4. 숙제내용 제출
-
미해결카카오,구글 SNS 로그인(springboot3, vue3)
인가 코드 발급(프론트 vs 백)
카카오 데브톡 응답에 따르면 프론트/백엔드 분할 책임 방식이 지양한다고 합니다.오히려 백에서 로그인 구현을 일임하는 것을 권장합니다.https://devtalk.kakao.com/t/oauth/136448 해당 블로그에서도 책임을 프론트와 백엔드가 나누어 가지는 방식이 잘못되었다고 합니다.https://cafe.naver.com/xxxjjhhh/296 카카오 로그인 REST API에서도 백에서 인가 코드를 받습니다.https://developers.kakao.com/docs/ko/kakaologin/rest-api#before-you-begin-process 어떤 방식이 맞는지 헷갈립니다.
-
해결됨카카오,구글 SNS 로그인(springboot3, vue3)
카카오 클라이언트 시크릿
안녕하세요 강사님,다름이 아니라 강의 들으면서 카카오 oauth 설정 및 테스트 중인데카카오 REST API 키에도 클라이언트 시크릿이 생긴 것 같습니다.구글처럼 yaml에 kakao.client.secret 키 넣어주고 요청 바디에 같이 보내니까 그제서야 액세스 토큰 및 프로필 정보 응답이 왔습니다.강의 촬영 당시와 현재의 카카오 developers UI 및 메뉴 구성이 많이 바뀌어서 제가 모종의 설정을 놓친건지, 아니면 새롭게 카카오 클라이언트 시크릿이 추가되어서 이제는 구글처럼 시크릿 키를 요청 바디에 넣고 보내는 게 맞는 방법이 된 건지 말씀 여쭤보고자 질문 드립니다.양질의 강의 항상 감사드립니다.
-
미해결Open API 사용 with 파이썬
pypi.org 에서 뭘 어떻게 깔아야 하는지 알려주세요
pypi.org 에서 뭘 어떻게 깔아야 하는지 알려주세요.알아서 깔으라고 하고 넘어가버렸는데 처음 접하는 사람은 뭘 깔아야 하는지 알수가 없어요.
-
미해결AI 시대에 살아남기: Supabase로 백엔드 뚝딱!
todos 테이블, RLS, 트리거 생성 미션 질문드립니다
정답으로 보여주신 이미지에는 아래와 같이 FK가 설정되어있는 것 같았는데 맞을까요?profiles.user_id -> auth.users.id (CASCADE) todos.user_id -> auth.users.id (CASCADE) todos.user_id -> profiles.users.id (CASCADE)todos.user_id -> profiles.users.id를 잇는 FK는 왜 필요한거고, 어떤 의도로 생성하는것인지 궁금합니다
-
미해결AI 시대에 살아남기: Supabase로 백엔드 뚝딱!
비개발자이고, 바이브코딩을 하는 40대중반 직장인입니다.
작년 12월 러바블을 시작으로 커서, 안티그래피 그리고 클로드코드 등을 활용하여 바이브코딩을 해왔습니다. 그러면서 백엔드의 중요성을 몸소 느끼게 되었죠. 아주 간단한 건 버셀과 수파베이스를 어찌저찌 ai에게 물어보면서 연동을 했지만, 전문적인 지식이 필요함을 느끼게 되었습니다. 그러던 중 기적처럼 어제 이 강의가 개설된 것을 알게 되었죠. 너무 감사합니다. 한번 열심히 배워보겠습니다. 비개발자 출신이기에 다소 어려울 수도 있겠지만 뭐 하다보면 되겠죠 .ㅎ
-
미해결AI 시대에 살아남기: Supabase로 백엔드 뚝딱!
감사히 잘 듣겠습니다.
수파베이스에 대한 갈증이 있었는데 타 플랫폼도 그렇고 한국강좌가 없어서 많이 아쉬워하고 있었는데요 인프런에 2만2천원짜리 강의하나밖에 없어서 그것을 듣긴했는데 그것만으론 많이 아쉬움을 느끼고 있던차에 이 강의가 나왔다는 메일을 받고 정말 기뻐서 바로 결제했습니다 수파베이스를 활용해서 만들고싶은 것들이 잔뜩 머릿속에 있거든요 감사히 잘 듣겠습니다.
-
미해결스프링 시큐리티 OAuth2
authorization-server 라이브러리 질문이 있습니다.
강의 내에서 dependencies 하고 있는spring-security-oauth2-authorization-server 랑 아래 org.springframework.boot:spring-boot-starter-oauth2-authorization-server 과는 다른 라이브러리인가요?프로젝트 생성하면서 oauth2 authorization 서버를 선택 후에 build.gradle를 확인했는데강의에서 나온 것과 상이하여 질문드립니다.
-
미해결스프링 시큐리티 OAuth2
loadUser 중 Missing attribute 'preferred_username' in attributes 에러 발생
55강 수강 중 client credentials 방식으로 변경 후 테스트 시 이런 에러가 발생합니다. 왜 그런걸까요 ㅜㅜ
-
미해결스프링 시큐리티 OAuth2
JWT 조회 에 대한 질문
안녕하십니까. 스프링 시큐리티 강의를 듣고 있는 대학생입니다. 현재 강의를 들으면서 팀 프로젝트에 소셜 로그인을 적용하던 중 궁금증이 생겨 글을 작성하게 되었습니다. 현재 사용자가 로그인 후 토큰을 발급받고 MySQL에 리프레시 토큰을 저장한 뒤에, 사용자가 API에 접근할 때마다 매번 사용자 정보를 조회하는 것이 좋은 방법인지 궁금합니다. 현재 매번 조회하는 이유는 토큰의 사용자 정보가 서버 데이터베이스에 올바르게 저장된 사용자인지 검증하기 위해서 이 방식을 채택했습니다 package backend_lingua.linguas.infrastructure.security.filter; import backend_lingua.linguas.infrastructure.security.token.enumerated.TokenType; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getBearerToken(request); // 토큰이 있는 경우에만 검증 if (StringUtils.hasText(token)) { try { if (jwtTokenProvider.validateAccessToken(token)) { Authentication authentication = jwtTokenProvider.getAuthentication(token, TokenType.ACCESS_TOKEN); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("Security Context에 '{}' 인증 정보를 저장했습니다.", authentication.getName()); } } catch (ExpiredJwtException e) { log.error("토큰 인증 실패: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed"); return; } } filterChain.doFilter(request, response); } private String getBearerToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } public TokenInfo generateToken(Authentication authentication) { String accessToken = createAccessToken(authentication); String refreshToken = createRefreshToken(authentication); Date accessTokenExpiryDate = createExpiryDate(accessTokenExpiration); return TokenInfo.from(accessToken, refreshToken, accessTokenExpiryDate.getTime(), refreshTokenExpiration); } public String createAccessToken(Authentication authentication) { return createToken(authentication, accessTokenExpiration, accessTokenSecret); } public String createRefreshToken(Authentication authentication) { String token = createToken(authentication, refreshTokenExpiration, refreshTokenSecret); Date expiryDate = createExpiryDate(refreshTokenExpiration); // DB에 리프레시 토큰 저장 tokenService.createRefreshToken(token, expiryDate, authentication); return token; } private String createToken(Authentication authentication, long expirationTime, String secretKey) { String authorities = authentication.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder() .setSubject(authentication.getName()) // email .claim("auth", authorities) .setIssuedAt(new Date()) .setExpiration(createExpiryDate(expirationTime)) .signWith(createKey(secretKey)) .compact(); } public String getUsernameFromToken(String token, TokenType tokenType) { String secretKey = (tokenType == TokenType.ACCESS_TOKEN) ? accessTokenSecret : refreshTokenSecret; return Jwts.parserBuilder() .setSigningKey(createKey(secretKey)) .build() .parseClaimsJws(token) .getBody() .getSubject(); } public Authentication getAuthentication(String token, TokenType tokenType) { String username = getUsernameFromToken(token, tokenType); UserPrincipal userPrincipal = (UserPrincipal) userDetailsService.loadUserByUsername(username); return new UsernamePasswordAuthenticationToken( userPrincipal, token, userPrincipal.getAuthorities() ); } public boolean validateAccessToken(String token) { return validateToken(token, accessTokenSecret); } public boolean validateRefreshToken(String token) { return validateToken(token, refreshTokenSecret); } private boolean validateToken(String token, String secretKey) { try { Jwts.parserBuilder() .setSigningKey(createKey(secretKey)) .build() .parseClaimsJws(token); return true; } catch (ExpiredJwtException e) { log.error("만료된 JWT 토큰입니다."); throw e; // Filter에서 401 처리하도록 } catch (SecurityException | MalformedJwtException e) { log.error("잘못된 JWT 서명입니다."); } catch (UnsupportedJwtException e) { log.error("지원되지 않는 JWT 토큰입니다."); } catch (IllegalArgumentException e) { log.error("JWT 토큰이 잘못되었습니다."); } return false; } private Date createExpiryDate(long expirationTime) { return new Date(System.currentTimeMillis() + expirationTime); } private SecretKey createKey(String secret) { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); } @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final MemberRepository memberRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email)); return UserPrincipal.create(member); } }public class TokenServiceImpl implements TokenService { private final RefreshTokenRepository refreshTokenRepository; private final MemberRepository memberRepository; @Override @Transactional public RefreshToken createRefreshToken(String token, Date expiryDate, Authentication authentication) { String email = authentication.getName(); Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email)); // 새 토큰 생성 RefreshToken refreshToken = RefreshToken.builder() .token(token) .member(member) .expiryDate(convertToLocalDateTime(expiryDate)) .build(); return refreshTokenRepository.save(refreshToken); } @Override @Transactional public void deleteRefreshToken(String email) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email)); deleteRefreshTokenByUser(member); } @Override @Transactional public void deleteRefreshTokenByUser(Member member) { refreshTokenRepository.findByMemberId(member.getId()) .ifPresent(refreshTokenRepository::delete); } private LocalDateTime convertToLocalDateTime(Date date) { return Instant.ofEpochMilli(date.getTime()) .atZone(ZoneId.systemDefault()) .toLocalDateTime(); } }
-
미해결스프링 시큐리티 OAuth2
password grant 방식 에러 응답
강의보고 실습하고 있는데요, password grant 타입은 아래와 같이 에러가 발생합니다. 유저 consent도 등록이 되어있는데, 진행이 안됩니다.
-
미해결스프링 시큐리티 OAuth2
FormLoginConfigure에서 생성하는 필터
FormLoginConfigure에서 생성하는 필터가 UsernamePasswordAuthenticationFilter라고 말씀해주셨는데(8:37) ㅎinitDefaultLoginFilter 메서드에서 생성되는 필터 클래스가 DefaultLoginPageGenerationgFilter인거같은데 맞을까요?
-
미해결스프링 시큐리티 OAuth2
현업에서 springboot를 3.5.5 를 사용해서 공부중인데...
httpSecurity 클래스의 메서드 authorizeHttpRequests, formLogin, apply메서드들의 인자값이 다 바뀌었네요 혹시 3.xx 버전으로 작성된 코드가 있을까요?
-
미해결카카오,구글 SNS 로그인(springboot3, vue3)
소셜 로그인 설계
안녕하세요 강사님 혹시 oauth2 를 이용한 로그인을 한 다음에 사용자 프로필을 받는 것이 서비스 플로우인데 이럴경우 웹이 아닌 앱을 사용할경우 (로직은 다른 거 알고 있습니다.) (PCKE 방식 사용 예정입니다.)oauth2 를 해서 사용자 db에 저장이 되고 이렇게 처음에 저장이 되고 그 다음에 프로필을 받으면 저 값이 수정이 되게 만들어 주는 게 맞을지 질문드립니다. 사용자엔티티에 Provider 나 socialId 로 값을 받습니다. (구글과 애플을 사용할 예정입니다.) @Entity @Table(name = "users") @Getter @NoArgsConstructor @Setter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; @Column(name = "name", nullable = false) private String name; @Enumerated(EnumType.STRING) @Column(name = "SEX", nullable = false) private Sex sex; @Column(name = "age", nullable = false) private Integer age; @Column(name = "nationality", nullable = false) private String nationality; @Column(name = "introduction", length = 40, nullable = false) private String introduction; @Column(name = "visit_purpose", length = 40, nullable = false) private String visitPurpose; @Column(name = "languages", nullable = false) private String languages; @Column(name = "hobby", nullable = false) private String hobby; @Column(name = "created_at", nullable = false) private Instant createdAt; @Column(name = "updated_at", nullable = false) private Instant updatedAt; @Column(name = "Provider", nullable = false) private String provider; @Column(name = "social_id", nullable = false) private String socialId; @Column(name = "email", nullable = false) private String email; @Builder public User(String name, Sex sex, Integer age, String nationality, String introduction, String visitPurpose, String languages, String hobby, String provider, String socialId, String email) { this.name = name; this.sex = sex; this.age = age; this.nationality = nationality; this.introduction = introduction; this.visitPurpose = visitPurpose; this.languages = languages; this.hobby = hobby; this.provider = provider; this.socialId = socialId; this.email = email; this.createdAt = Instant.now(); this.updatedAt = Instant.now(); } public void updateProfile(UserUpdateDTO dto) { if (dto.getName() != null && !dto.getName().trim().isEmpty()) { this.name = dto.getName().trim(); } if (dto.getSex() != null) { this.sex = dto.getSex(); } if (dto.getAge() != null) { this.age = dto.getAge(); } if (dto.getNationality() != null && !dto.getNationality().trim().isEmpty()) { this.nationality = dto.getNationality().trim(); } if (dto.getIntroduction() != null && !dto.getIntroduction().trim().isEmpty()) { this.introduction = dto.getIntroduction().trim(); } if (dto.getVisitPurpose() != null && !dto.getVisitPurpose().trim().isEmpty()) { this.visitPurpose = dto.getVisitPurpose().trim(); } if (dto.getLanguages() != null && !dto.getLanguages().trim().isEmpty()) { this.languages = dto.getLanguages().trim(); } if (dto.getHobby() != null && !dto.getHobby().trim().isEmpty()) { this.hobby = dto.getHobby().trim(); } this.updatedAt = Instant.now(); // 수정 시각 갱신 } }
-
미해결카카오,구글 SNS 로그인(springboot3, vue3)
카카오 인가코드 요청
카카오 인가 코드 요청 url이 변경 된 것 같습니다!
-
해결됨카카오,구글 SNS 로그인(springboot3, vue3)
구글 로그인 프론트 화면
안녕하세요 구글 로그인 구현 중 질문있어서 질문드립니다! googleUrl, googleClientId, googleRedirectUrl, googleScope, googleResponseType 을 const auth_url = `${this.googleUrl}?client_id=${this.googleClientId}&redirect_url=${this.googleRedirectUrl}&response_type=code&scope=${this.googlescope}`; window.location.href = auth_url; 구글로그인 화면을 누르면 리다이렉트 url에 오류가 있다고 하는데 동일한 리다이렉트 url로 해도 잘 안됩니다 이럴 경우 또 어떤 것을 확인해보는게 좋을까요??
-
미해결카카오,구글 SNS 로그인(springboot3, vue3)
github 주소 어디서 확인가능한가요?
소스코드 올라온 github 주소 어디서 확인이 가능할까요?
-
미해결카카오,구글 SNS 로그인(springboot3, vue3)
안녕하세요 선생님
혹시 다음 강의도 준비 중이신가요 ? 준비 중이시라면 어떤 강의 인지 알 수 있을까요