SSAFY의 첫 프로젝트인 ‘SHabit’ 에서는 Spring Security 를 통한 자체 로그인, 카카오, 네이버, 구글 총 네 개의 로그인 시스템을 구축했습니다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
/oauth/authorize 에서 사용자 인증 후 /oauth/token 을 통해 토큰을 발급받아 사용자의 정보를 받아볼 수 있습니다.
spring:
security:
oauth2:
client:
registration:
kakao:
client-id: client-id-key
client-secret: secret-value
scope:
- profile_nickname
- account_email
- profile_image
client-name: Kakao
authorization-grant-type: authorization_code
redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
client-authentication-method: POST
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
httpSecurity
.oauth2Login()
.authorizationEndpoint()
.baseUri("/oauth2/authorization")
.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository())
.and()
.redirectionEndpoint()
.baseUri("/*/oauth2/code/*")
.and()
.userInfoEndpoint()
.userService(oAuth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailureHandler());
userService() 메서드에서 사용자의 정보를 받게 됩니다..successHandler(oAuth2AuthenticationSuccessHandler()) 핸들러를 호출하여 JWT 생성 단계로 넘어갑니다.public class CustomOAuth2UserService extends DefaultOAuth2UserService {
...
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
....
}
}
OAuth2UserInfo 클래스를 정의하여 필수 정보를 담도록 하였습니다. 하지만 카카오, 네이버, 구글 등 세 가지 Provider에서 제공하는 유저 정보 데이터 형태가 조금씩 달라서 따로 데이터를 받기 위한 하위 클래스를 작성해야 합니다.// 사용자 정보를 호출할 때
...
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(providerType, user.getAttributes());
...
// switch 문으로 type에 맞게 처리
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(ProviderType providerType, Map<String, Object> attributes) {
return switch (providerType) {
case GOOGLE -> new GoogleOAuth2UserInfo(attributes);
case NAVER -> new NaverOAuth2UserInfo(attributes);
case KAKAO -> new KakaoOAuth2UserInfo(attributes);
default -> throw new IllegalArgumentException("Invalid Provider Type.");
};
}
}
# Kakao에서 유저 nickname을 가져오는 방식
public class KakaoOAuth2UserInfo extends OAuth2UserInfo {
...
@Override
public String getName() {
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
if (properties == null) {
return null;
}
return (String) properties.get("nickname");
}
...
}
// Access Token 생성
String accessToken = Jwts.builder()
.setSubject(uuId)
.claim(USER_ID, userId)
.claim(AUTHORITIES_KEY, role)
.claim(CREATED_TIME, createdTime)
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(new Date(now + ACCESS_TOKEN_EXPIRE_TIME))
.compact();
// Refresh Token 생성
String refreshToken = Jwts.builder(
.setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
setExpiration 를 통해 토큰의 유효 기간을 설정합니다.private final RedisProperties redisProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
redisTemplate.opsForValue()
.set("token:" + authentication.getName(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);