OAuth2 Deepdive
๐ Oauth2 ์ ์ฉ ์
๐ Oauth2 ์ค์ต
๊ฐ์
์ต๊ทผ ๊ฐ์ธ ํ๋ก์ ํธ์์ ๋ค์ด๋ฒ ๋ก๊ทธ์ธ์ ์ํด ํ์ํ ๋ถ๋ถ์ ์ค์ ํ๊ณ ๋ก์ง์ ๊ตฌํํ๋ค. ๊ฐ๋ฐ ๊ฐ์ด๋์ ๋ง๊ฒ ๊ฐ ๋จ๊ณ๋ณ๋ก ์งํ์ ํ์ผ๋โฆ
์๋ฌด๋ฆฌ ๋ด๋ ๊ณผ์ ํ๋ ํ๋๋ฅผ ๋ด ์์ผ๋ก ์ง์ ๊ตฌํํ๋๊ฒ ๋ง๋๊ฑด๊ฐ? ๋ผ๋ ์๋ฌธ์ ์์์ผ๋ก ์ข ๋ ํจ์จ์ ์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ฐพ์๋ดค๊ณ , ๊ทธ๋ ๊ฒ Oauth 2.0
์ผ๋ก ๋ก๊ทธ์ธํ๋ ๋ฐฉ์์ ์ฐพ๊ฒ ๋์๋ค.
Oauth2 ์ ์ฉ ์
์ฝ๋ ์คํ์ผ์ ๋ ๋์ ์ง๊ธ ์์ ๋ณด๋ ๋ง์น Oauth 1.0
์ฒ๋ผ ๊ตฌํ์ ํ๊ณ ์์๋ค. ๋ ๋ํ, ๊ฐ ํ๋ก์ฐ๋ง๋ค ํ๋ ์ฝ๋ฉ์ ์ค์ด๊ณ ๋ณ์๋ ํจ์๋ฅผ ์ฌํ์ฉํ๊ธฐ ์ํด, ๋ณด์ ์์๋ฅผ ์๊ฐํ๋ฉด์ ๊ตฌํํ์๋ค.
์๋ ํํฐ ๊ด๋ จ API๋ฅผ ํธ์ถํ ๋, ์๋ต ๊ฐ์ Json์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ ์ํด JsonNode
๋ฅผ ๋ง์ด ์ฌ์ฉํ๋๋ฐ ๋์ผํ๊ฒ ์ ์ฉํ๋ ค๊ณ ๋ณด๋ ์์ค๊ฐ ๋๋ฌด ๊ธธ์ด์ง ๊ฒ ๊ฐ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
private final NaverApiService naverApiService;
private final SnsInfoService snsInfoService;
private final UserService userService;
public String getNaverOauth2LoginUrl(HttpServletRequest request) {
String authorizeUrl = BASE_URL+"/authorize";
String callbackUrl = getServiceUrl()+"/login/naver/callback";
String state = NakjiUtil.generateTokenState();
request.getSession().setAttribute("state", state);
return UriComponentsBuilder
.fromHttpUrl(authorizeUrl)
.queryParam("response_type", "code")
.queryParam("client_id", secrets.naver().naverId())
.queryParam("redirect_uri", callbackUrl)
.queryParam("state", state)
.toUriString();
}
public String processNaverLogin(LoginProfile profile, Oauth2AccessToken tokenInfo) {
SnsInfo checkSnsInfo = snsInfoService.getOrCreateUser(profile, tokenInfo);
Optional<User> user = userService.getUserByUserId(checkSnsInfo.getSnsId());
if (user.isPresent()) {
// Update user info
} else {
// Create user
}
return "redirect:/";
}
public String processNaverLoginCallback(String state, String code, HttpSession session) {
Oauth2AccessToken token = getToken(state, code, session);
if (token == null) {
throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
}
try (Response response = naverApiService.naverProfileConnection(token.tokenType(), token.accessToken())) {
if (response.status() == 200) {
JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream());
if ("00".equals(resultJson.get("resultcode")) && "success".equals(resultJson.get("message"))) {
JsonNode profileInfo = resultJson.get("response");
LoginProfile profile = new LoginProfile("naver",
Optional.ofNullable(profileInfo.get("id")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(profileInfo.get("nickname")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(profileInfo.get("gender")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(profileInfo.get("age")).map(JsonNode::asText).orElse("")
);
return processNaverLogin(profile, token);
} else {
throw new Exception();
}
} else {
log.info("NaverLoginService.getNaverProfileUrl Request Status: {}, Body: {}", response.status(), response.body().toString());
//throw new BadRequestException("Bad request with status: " + response.status());
}
} catch (Exception e) {
log.error("NaverLoginService.getNaverProfileUrl Error: ", e);
}
return "";
}
public Oauth2AccessToken getToken(String state, String code, HttpSession session) {
try (Response response = getGenerateToken(state, code, session)) {
if (response.status() == 200) {
JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream());
Oauth2AccessToken resultToken = new Oauth2AccessToken(
Optional.ofNullable(resultJson.get("access_token")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(resultJson.get("refresh_token")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(resultJson.get("token_type")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(resultJson.get("expires_in")).map(JsonNode::asInt).orElse(0),
Optional.ofNullable(resultJson.get("error")).map(JsonNode::asText).orElse(""),
Optional.ofNullable(resultJson.get("error_description")).map(JsonNode::asText).orElse("")
);
if ("invalid_request".equals(resultToken.errorCode())) {
throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
} else if ("unauthorized_client".equals(resultToken.errorCode())) {
throw new UnauthorizedException(ErrorCode.INVALID_AUTH_CODE);
}
return resultToken;
} else {
log.info("NaverLoginService.getToken Request Status: {}, Body: {}", response.status(), response.body().toString());
//throw new BadRequestException("Bad request with status: " + response.status());
}
} catch (Exception e) {
log.error("NaverLoginService.getToken Error: ", e);
}
return null;
}
private Response getGenerateToken(String state, String code, HttpSession session) {
String storedState = (String)session.getAttribute("state");
if (!state.equals(storedState)) {
throw new UnauthorizedException(ErrorCode.INVALID_AUTH_PATH);
}
return Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(NaverGenerateTokenClient.class, BASE_URL)
.generateToken(secrets.naver().naverId(), secrets.naver().naverKey(), state, code);
}
Oauth2 ์ ์ฉ ํ
gpt์ ์ผ๋ถ ๋ธ๋ก๊ทธ๋ฅผ ์ฐพ์๋ณด๋ฉด์ ๋ด ๋ฐฉ์๋๋ก ์ ์ฉํด๋ดค๋ค. ์ฐ์ ๋ก๊ทธ์ธ์ ์ ๋๊ณ , ์ด๊ธฐ ์ธํ ๋ง ํด๋๋ฉด ๊ทธ ์ดํ์๋ ์์ค๋ฅผ ์ฌํ์ฉํ๊ฑฐ๋ ์กฐ๊ธ๋ง ๋ ์ถ๊ฐํ๋ ๊ฒ์ผ๋ก ํด๊ฒฐ๋ ๊ฒ ๊ฐ๋ค.
๋ค์์๋ ํ์ด์ง๋ง๋ค ์ญํ ๋ถ์ฌํ๋ ๊ฒ๊ณผ jwt
์ ์ด์ฉํ ๋ก๊ทธ์ธ์ ์ถ๊ฐํ ์์ ์ด๋ค.
security.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
security:
oauth2:
client:
registration:
naver:
client-id: ${NAVER_CLIENT_ID}
client-secret: ${NAVER_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: ${SERVICE_URL}${NAVER_REDIRECT_URI}
scope: name, nickname, email, gender, birthyear, profile_image
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
SecurityConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
...
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.build();
}
}
OAuthAttributes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Builder
public record OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, User user) {
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
if ("naver".equals(registrationId)) {
return ofNaver(userNameAttributeName, attributes);
} else {
return null;
}
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.user(User.builder()
.oauthId((String) response.get("id"))
.provider("naver")
.name((String) response.get("name"))
.nickname((String) response.get("nickname"))
.profileImage((String) response.get("profile_image"))
.email((String) response.get("email"))
.gender((String) response.get("gender"))
.birthYear((String) response.get("birthYear"))
.role(Role.USER)
.build())
.build();
}
public User toEntity() {
return user;
}
}
OAuth2UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
private User saveOrUpdate(OAuthAttributes attributes) {
User userInfo = attributes.getUser();
User target = userRepository.findByOauthId(userInfo.getOauthId())
.map(entity -> entity.update(userInfo.getNickname(), userInfo.getProfileImage(), userInfo.getEmail()))
.orElse(attributes.toEntity());
return userRepository.save(target);
}
}
ํ๊ธฐ
์ฌ๋ด์ด์ง๋ง ํค๊ฐ์ ๊ด๋ฆฌํ ๋, ํด๋น ์ค์ ํ์ผ์ ์ ์ฅ์์๋ ์ปค๋ฐํ์ง ์์๋ค. ๋ค๋ฅธ PC์์ ์ ์ํ ๋๋ ๊ฐ์ธ ํด๋ผ์ฐ๋์์ ํค๊ฐ์ด ์๋ ํ์ผ์ ๋ถ๋ฌ์์ ์ฌ์ฉํ๋๋ฐ, ์ด๋ฒ์ oauth๋ฅผ ๊ฒฝํํ๋ฉด์ ์ ์ ๋ถํธํด์ง ์ ์์ ๊ฒ ๊ฐ๋ค.
๊ทธ๋์ dotenv
๋ฅผ ์ฌ์ฉํ๊ณ ๊ธฐ์กด ํ๊ฒฝ์ค์ ๊ฐ๋ค์ env ํ์ผ์์ ๊ฐ์ ธ์ค๋ ๋ฐฉ์์ผ๋ก ์์ ํ๋ค.
ํฐ ์ฐจ์ด๋ ์์ ์ ์์ง๋ง, ๋ด๊ฐ ํ๊ฒฝ์ค์ ํ ๋ ์ด๋ค ๊ตฌ์กฐ๋ก ์ค์ ํ๋์ง๋ ๋ณผ ์ ์๊ฒ ๋ ๊ฒ์ด ๊ฐ์ ์ ์ด๋ค.