작성
·
1.1K
0
SecurityConfig
package 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);
}
}
JwtProvider
package 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;
}
}
JwtAuthenticationFilter
package 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 생략
PrincipalDetails
package 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;
}
}
PrincipalDetailsService
package 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);
}
}
OAuth2UserInfo
package com.example.project1.config.oauth2.provider;
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
PrincipalOauth2UserService
package 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());
}
}
GoogleUserInfo
package 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");
}
}
NaverUserInfo
package 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);
}
}
TokenDTO
package 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;
}
}
TokenEntity
package 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;
}
}
MemberService
package 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이 뜹니다.... 어떻게 해야할까요
답변 1
0
안녕하세요, 인프런 AI 인턴이에요. 소셜 로그인 후 JWT 발급과 관련된 내용을 알려드릴게요.
소셜 로그인 후 JWT 발급을 구현하려면 몇 가지 단계를 거쳐야 합니다. 먼저, Spring Security의 PrincipalOauth2UserService
를 사용하여 소셜 로그인에서 제공하는 사용자 정보를 가져와야 합니다. 이 정보를 사용하여 JWT token을 생성하고 발급해야 합니다.
위에 작성된 코드를 보면 PrincipalOauth2UserService
가 구현되어 있고, 그 외에도 JwtProvider
가 사용되고 있습니다. PrincipalOauth2UserService
는 OAuth2 소셜 로그인 정보를 처리하고 사용자 인증을 구현하는 데 도움을 주는 클래스입니다. JwtProvider
는 JWT 토큰의 생성 및 검증을 담당하는 클래스입니다.
JWT 토큰을 만들기 위해 사용하는 JwtProvider
클래스에는 generateToken()
메서드가 있습니다. 여기서 사용자 정보를 활용하여 토큰을 생성하고 반환합니다. 이 토큰은 소셜 로그인 이후에 클라이언트에게 제공될 것입니다.
따라서, Spring Security의 PrincipalOauth2UserService
와 JwtProvider
를 적절히 구현하고, 로그인 로직에서 JWT 토큰을 발급하는 코드를 추가해야 합니다.
그러나 위의 코드만으로는 전체적인 로직을 파악하기 어려우므로, 더 자세한 코드나 설정 내용이 필요하다면 해당 부분도 함께 공유해주시면 더 정확한 답변을 드릴 수 있을 것 같아요. 감사합니다.
https://github.com/YuYoHan/project_study1 전체 링크입니다.
일단, 시큐리티 컨피그 중 Oauth2에 관련된 부분입니다. 방식은 REST 방식입니다.
저는 현재 생각하는 방식이 소셜 로그인을 성공하면 PrincipalOauth2UserService 에서 Member 테이블에 넣어주고 save까지 해서 로직을 구현했습니다. 그리고 실행해본 결과 로그로 제대로 값이 등록되어 있는 것을 볼 수 있었습니다.
이제 @AuthenticationPrincipal OAuth2User oAuth2User를 사용하면 소셜 로그인한 정보를 가져와서 JWT 발급하는데 사용할 수 있다고 들었는데
@AuthenticationPrincipal OAuth2User oAuth2User를 사용해서 정보를 받아와서 null이 아닐 경우
MemberService
JwtProvider
이런식으로 로직을 짰는데 지금 생각하는 방식이 소셜 로그인이 성공했을 경우 주소, 권한(USER or ADMIN) 를 추가적으로 넣고 싶은데 소셜 로그인으로 어떻게 해야할지 감이 안잡힙니다 ㅠㅠ
그리고 컨트롤러에서 제대로 정보를 못가지고 오는거 같습니다.
PrincipalOauth2UserService에서 return new PrincipalDetails(member, oAuth2User.getAttributes());이렇게 보내줬는데
PrincipalDetails에서
생성자를 구현했지만 컨트롤러에서 못받고 있습니다.
추가 질문)
postman에서 소셜로그인 테스트를 해서 성공해서 accesstoken을 받았는데 거기서 뭘 어떻게 진행해야 하나요? 거기서 소셜 로그인을 성공하면 바로 토큰을 발급해줘서 요청시 header에 담아서 보내주것을 어떤식으로 해야하나요??
추가 질문2)
이런 에러가 발생합니다....
보니까
여기서 오류가 생기는거 같습니다. log.info("auth in JwtProvider : " + auth); 찍으면 auth in JwtProvider : [ROLE_USER] 이렇게 나옵니다