들어가기에 앞서
프로젝트를 진행도중 Github OAuth 2.0을 도입하고자 처음부터 마음먹었다.
그런데 OAuth가 처음이라 개념이 너무 헷갈리기도 했고 Github OAuth2.0을 대상으로 커스텀한 글이 많이 적어 시간이 많이 오래걸렸다.
다른 글들을 많이 참고하고 비슷한 부분이 많지만 그래도 Github로 한 것은 없기에 글을 써봅니다.
https://velog.io/@jkijki12/Spring-Boot-OAuth2-JWT-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EB%A6%AC%EA%B8%B0
해당글을 참고해서 쓴 글입니다!!
OAuth 구조에 대한 많은 의견이 있지만 저는 이렇게 구현하기로 했습니다.
많은 과정이있지만 간략하게 코드와, 주석, 설명으로 대체하겠습니다
일단 간략하게 흐름을 보자면
1. localhost:8080/login으로 이동한다 -> localhost:8080/oauth2....?redirect(내가 Github에서 롤백받을 주소)로 이동합니다
2. 로그인, 권한인증을한다(Client시점)
3. 인가코드를 redirect한 주소로 받아서 -> Github에게 다시 쏜다 -> Github가 AccessToken을 준다 -> 다시 Github에게 쏜다
-> 유저정보를 넘겨준다.
4. 넘겨준 유저정보를 바탕으로 DB에 저장하고 Token을 발급해서 프론트에게 쏜다.
이런 흐름인데 여기서 저희는 인가코드, AccessToken받는 과정을 뛰어넘을 수 있습니다 -> Oauth2UserService안에 메소드들을 Overide해서
정확히는 Github에게 인가코드를 받고 인가코드를 Oauth2UserService가 가로채서 사용자 정보를 Github에게서 받아옵니다.
3번에서 번거로운 요청을 Oatuh2UserService가 대신 해줍니다
바로 코드를 봅시다
Security Config
-> WebSecurityConfigurerAdapter를 Deparated됐기 때문에 상속받지 않습니다
@Bean
protected SecurityFilterChain config(HttpSecurity http, JwtProvider jwtProvider,
CookieUtil cookieUtil) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/home/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(jwtProvider, cookieUtil, refreshRepository),
UsernamePasswordAuthenticationFilter.class)
.oauth2Login()
.authorizationEndpoint()
.baseUri("/login")
.and()
.redirectionEndpoint()
.baseUri("/auth/code")
.and()
.successHandler(successHandler)
.userInfoEndpoint()
.userService(customOAuth2UserService);
return http.build();
}
중요한 것만 설명하자면
- oauth2Login().authorizationEndpoint().baseUri("/login) -> Oauth2.0 인증에서 사용자가 로그인할 때 기본 Uri는 "login"
즉 localhost:8080/login하면 로그인페이지로 이동합니다.
- redirectionEndPoint().baseUri("/login") 인증을 완료하면 해당 Uri로 이동합니다.
- successHandler() 인증 성공시 처리하는 핸들러 Custom한 핸들러를 사용
- userService() 사용자 정보 객체를 가져오고 가져왔을때 어떤 Service파일을 쓸 것이냐 Custom한 것으로 사용
OAuth2UserService
여기서 OAuth2UserService는
Github에게 인가코드를 받고 인가코드를 OAuth2UserService가 가로채서 사용자 정보를 Github에게서 받아옵니다.
인가코드는 Github에서 설정한 주소로 받기만 하면 알아서 인가코드를 가로챔
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1번
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
// 2번
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
// 3번
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
log.info("registrationId = {}", registrationId);
log.info("userNameAttributeName = {}", userNameAttributeName);
log.info(String.valueOf(oAuth2User));
// 4번
OAuth2Attribute oAuth2Attribute =
OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
var memberAttribute = oAuth2Attribute.convertToMap();
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
memberAttribute, "id");
}
}
1. DefaultOAuth2UserService 객체를 성공정보바탕으로 만든다.
2. 생성된 Service객체로부터 User객체받기
3. 받은 User로부터 정보 받기
4. SuccessHandler사용가능하도록 등록
OAuth2 Atrribute
@ToString
@Builder(access = AccessLevel.PRIVATE)
@Getter
public class OAuth2Attribute {
private Map<String, Object> attributes;
private String attributeKey;
private String email;
private String name;
private Long id;
public static OAuth2Attribute of(String provider, String attributeKey,
Map<String, Object> attributes) {
switch (provider) {
case "github":
return ofGithub("id", attributes);
default:
throw new RuntimeException();
}
}
private static OAuth2Attribute ofGithub(String attributeKey,
Map<String, Object> attributes) {
Integer id = (Integer) attributes.get("id");
return OAuth2Attribute.builder()
.id(Long.valueOf(id))
.name((String) attributes.get("Name"))
.email((String) attributes.get("email"))
.attributes(attributes)
.attributeKey(attributeKey)
.build();
}
public Map<String, Object> convertToMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", attributeKey);
map.put("key", attributeKey);
map.put("name", name);
map.put("email", email);
map.put("id", id);
return map;
}
}
우리는 Github만 쓸 것이기 때문에 Github만 구현했습니다.
OAuth2SuccessHandler
@Slf4j
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtProvider tokenService;
private final MemberRepository userRequestMapper;
private final ObjectMapper objectMapper;
@Autowired
private MemberRepository memberRepository;
@Autowired
private RefreshRepository refreshRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
OauthMemberDto userDto = new OauthMemberDto();
userDto.setId((Long) oAuth2User.getAttributes().get("id"));
userDto.setName((String) oAuth2User.getAttributes().get("name"));
userDto.setEmail((String) oAuth2User.getAttributes().get("email"));
userDto.setCreated(new Date());
log.info("Principal에서 꺼낸 OAuth2User = {}", oAuth2User);
// 최초 로그인이라면 회원가입 처리를 한다.
String targetUrl;
log.info("토큰 발행 시작");
HashMap<String, String> m = new HashMap<>();
m.put("githubId", String.valueOf(userDto.getId()));
Token token = new Token();
token.setAccessToken(tokenService.generateToken(m));
token.setRefreshToken(tokenService.generateRefreshToken(m));
String ip = request.getRemoteAddr();
Refresh refresh = new Refresh();
Member member = new Member();
member.setId((Long) oAuth2User.getAttributes().get("id"));
member.setName(String.valueOf(oAuth2User.getAttributes().get("name")));
member.setEmail(String.valueOf(oAuth2User.getAttributes().get("email")));
member.setCreated(new Date());
member.setUpdated(new Date());
member.setRoles(Collections.singletonList("USER"));
member.setPassword("");
refresh.setMember(member);
refresh.setRefreshToken(token.getRefreshToken());
refresh.setIp(request.getRemoteAddr());
refresh.setId(member.getId());
member.setRefresh(refresh);
refreshRepository.save(refresh);
memberRepository.save(member);
log.info("{}", token);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString("/home")
.queryParam("accessToken", token.getAccessToken())
.queryParam("refreshToken", token.getRefreshToken());
String redirectUrl = uriBuilder.toUriString();
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}
이곳을 왔다는 건, 로그인이 성공했다는 뜻입니다
1. 최초 로그인인지 확인
2. AccessToken, RefreshToken 생성 및 발급
3. token을 포함해서 리다이렉트 시키기
Jwt Provider, Filter는 코드는 자신의 코드에 맞게 커스텀하시면 될 거 같습니다.
Domain
UserDto
@Setter
@Getter
public class OauthMemberDto {
private Long id;
private String name;
private String email;
private Date created;
private List<String> roles = new ArrayList<>();
}
TokenDto
@ToString
@NoArgsConstructor
@Getter
@Setter
public class Token {
private String accessToken;
private String refreshToken;
public Token(String token, String refreshToken) {
this.accessToken = token;
this.refreshToken = refreshToken;
}
}
이렇게하면 다시 순서대로 해봅시다
localhost:8080/login -> 로그인페이지 이동후 로그인 -> 권한 인증 -> OAuth2UserService -> SuccessHandler -> 리다이렉트 시켜서 AccessToken, RefreshToken 얻기
+
프론트에게 리다이렉트 할 거라면 localhost:3000/accessToken:~&refreshToken:~ 해서 넘겨줍시다.
'스프링' 카테고리의 다른 글
[Spring] MySQL DB Replication 설정하기 (1) | 2024.12.09 |
---|---|
[Spring] TDD vs BDD 무엇인지알고 비교하기 (0) | 2023.03.12 |
[Spring] Slice를 이용하여 무한스크롤 구현하기 (0) | 2023.02.12 |
[Spring] Spring Security이용한 JWT 로그인 구현기 (1) | 2023.01.28 |
[Spring] JWT Refresh Token 어디에 저장해야 할까? 그리고 꼭 저장해야 할까? (0) | 2023.01.25 |