05-16 05:32
Recent Posts
Recent Comments
๊ด€๋ฆฌ ๋ฉ”๋‰ด

miinsun

[Lehgo] Spring boot : OAuth SNS Login, sns ํšŒ์›๊ฐ€์ž… ๋กœ๊ทธ์ธ ๋ณธ๋ฌธ

Project/2022 Lehgo

[Lehgo] Spring boot : OAuth SNS Login, sns ํšŒ์›๊ฐ€์ž… ๋กœ๊ทธ์ธ

miinsun 2022. 4. 5. 15:25

๐Ÿ’ป ์‹ค์Šต ํ™˜๊ฒฝ

Framework: Spring Boot, Spring Security
Build: Maven

 

๐Ÿ’ฌ ์š”๊ตฌ ์‚ฌํ•ญ & ์„œ๋น„์Šค ํ๋ฆ„

Google, Naver, Kakao API๋ฅผ ์ด์šฉํ•ด ํšŒ์› ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๊ณ  ๊ทธ ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ์„œ๋น„์Šค์— ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธํ•˜์ž

1. Google, Naver, Kakao์— application ๋“ฑ๋ก์„ ํ•˜๊ณ  callback url์„ ์ž…๋ ฅํ•จ
2. ๋กœ๊ทธ์ธ ์‹œ, Google, Naver, Kakao์— AccessToken์„ ์š”์ฒญํ•œ๋‹ค.
3. ์„ฑ๊ณต์ ์œผ๋กœ AccessToken์„ ๋ฐ›์œผ๋ฉด callBack์ฃผ์†Œ๋กœ ์ด๋™ํ•œ๋‹ค. 
4. AccessToken์„ ์ด์šฉํ•ด ๊ฐœ์ธ ์ •๋ณด๋ฅผ ์š”์ฒญํ•œ๋‹ค.
5. ๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›์ด๋ฉด ํšŒ์›๊ฐ€์ž…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
6. ์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ด๋ฉด ๋กœ๊ทธ์ธ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
7. ๋กœ๊ทธ์ธ์‹œ jwtToken์„ ๋ฐœ๊ธ‰ํ•œ๋‹ค.

 

์•„๋ž˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋Š” Naver OAuth๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋์Šต๋‹ˆ๋‹ค. ๊ฐ OAuth๋งˆ๋‹ค ์š”๊ตฌํ•˜๋Š” ์กฐ๊ฑด์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๊ผญ ๊ณต์‹๋ฌธ์„œ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

๋” ๊ตฌ์ฒด์ ์ธ ์ฝ”๋“œ๋Š” ์•„๋ž˜์˜ ๊นƒ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

 

GitHub - miinsun/Lehgo

Contribute to miinsun/Lehgo development by creating an account on GitHub.

github.com

 


 

๐Ÿ“Œ ๋Œ€/์†Œ๋ฌธ์ž converter

Controller์—์„œ ๋ฐ›๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ enumํƒ€์ž…์œผ๋กœ ๋Œ€๋ฌธ์ž๋ฅผ ์†Œ๋ฌธ์ž๋กœ ๋งคํ•‘ ํ•˜๋„๋ก converter๋ฅผ ์ƒ์„ฑ

 

@Configuration
public class SocialLoginTypeConverter implements Converter<String, SocialLoginType> {
    @Override
    public SocialLoginType convert(String s) {
        return SocialLoginType.valueOf(s.toUpperCase());
    }
}

 

๐Ÿ“Œ Redirect URL ์ƒ์„ฑ

๊ฐ API์˜ ๋ฉ”ํƒ€ ์ •๋ณด๋“ค์„ property์˜ ์„ฑ๊ฒฉ์œผ๋กœ application-API_KEY์— ์ €์žฅํ•ด resource๋กœ ๊ด€๋ฆฌํ•ด์ค๋‹ˆ๋‹ค. ๊ฐ๊ฐ ์ด๋•Œ API ๊ณต์‹ ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด APIServer์— redirect ์š”์ฒญ์„ ํ•˜๋„๋ก ํ—ค๋”๋ฅผ ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค.

@Override
    public String getOauthRedirectURL() {
		Map<String, Object> params = new HashMap<>();
        params.put("scope", getScopeUrl());
        params.put("response_type", "code");
        params.put("client_id", GOOGLE_CLIENT_ID);
        params.put("redirect_uri", GOOGLE_CALLBACK_URL);

        String parameterString = params.entrySet().stream()
                .map(x -> x.getKey() + "=" + x.getValue())
                .collect(Collectors.joining("&"));

        return GOOGLE_SNS_BASE_URL + "?" + parameterString;
    }

 

๐Ÿ“Œ Access Token ์ƒ์„ฑ

Spring Boot RestTemplate๋ฅผ ํ™œ์šฉํ•ด ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„. RestTemplate๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•œ ๊ฐ์ฒด mappingํ•ด ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. API ์„œ๋ฒ„๋ณ„ ๋ฏธ๋ฆฌ ์ง€์ •๋œ ํ˜•์‹์œผ๋กœ DTO๋ฅผ ๋งŒ๋“ค๊ณ , ํ† ํฐ ์š”์ฒญ ๊ฒฐ๊ณผ์™€ ๋งคํ•‘ ์‹œ์ผœ์ค๋‹ˆ๋‹ค.

ResponseEntity<NaverLoginResponse> apiResponseJson = restTemplate.exchange(
    NAVER_SNS_BASE_URL + "/token",
    HttpMethod.POST,
    naverTokenRequest,
    NaverLoginResponse.class
    );

 

 

๐Ÿ“Œ ์‚ฌ์šฉ์ž ์ •๋ณด ํ™•์ธ

๋ฐฉ๊ธˆ ์–ป์€ Access Token์„ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. Access Token ์ •๋ณด๋ฅผ header์— ์„ธํŒ…ํ•ด์ฃผ๊ณ , ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ api์„œ๋ฒ„์— GET ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€๊ณตํ•ด lehgo ์„œ๋น„์Šค์— ์ž๋™์œผ๋กœ ํšŒ์›๊ฐ€์ž…ํ•˜๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

/* ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */
// Http Header ์„ค์ •
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + naverLoginResponse.getAccessToken());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

HttpEntity<MultiValueMap<String, String>> naverTokenRequest2 = new HttpEntity<>(headers2);
ResponseEntity<NaverLoginDto> apiResponseJson2 = restTemplate.exchange(
        "https://openapi.naver.com/v1/nid/me", 
        HttpMethod.GET,
        naverTokenRequest2,
        NaverLoginDto.class
        );

 

 

๐Ÿ“Œ ์‚ฌ์šฉ์ž ์ •๋ณด ํ™•์ธ

๋ฐฉ๊ธˆ ์–ป์€ Access Token์„ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. Access Token ์ •๋ณด๋ฅผ header์— ์„ธํŒ…ํ•ด์ฃผ๊ณ , ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ api์„œ๋ฒ„์— GET ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€๊ณตํ•ด lehgo ์„œ๋น„์Šค์— ์ž๋™์œผ๋กœ ํšŒ์›๊ฐ€์ž…ํ•˜๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

/* ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */
// Http Header ์„ค์ •
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + naverLoginResponse.getAccessToken());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

HttpEntity<MultiValueMap<String, String>> naverTokenRequest2 = new HttpEntity<>(headers2);
ResponseEntity<NaverLoginDto> apiResponseJson2 = restTemplate.exchange(
        "https://openapi.naver.com/v1/nid/me", 
        HttpMethod.GET,
        naverTokenRequest2,
        NaverLoginDto.class
        );

 

 

๐Ÿ“Œ Spring Security ์„ค์ •

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ๊ฐ์ข… ์„ค์ •์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค์ •, ์ธ์ฆ ๋กœ์ง ์„ค์ •์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • .antMatchers("/admin/**").hasRole("ADMIN")
    • ๋ฆฌ์†Œ์Šค admin์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  url์€ ์ธ์ฆ ํ›„ ADMIN ๋ ˆ๋ฒจ์˜ ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•œ๋‹ค.
  • sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ์„ธ์…˜ ์ •์ฑ…์ž…๋‹ˆ๋‹ค. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜์ง€๋„ ์•Š๊ณ  ๊ธฐ์กด์— ๋งŒ๋“ค์–ด์ง„ ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. JWT ํ† ํฐ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด stateless ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค.
  • http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    • Spring Security Filter์™€ ํ†ตํ•ฉํ•˜์ง€ ์•Š๊ณ  JwtAuthenticationFilter์—์„œ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ์ž‘์—…์„ ์ง„ํ–‰ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— JwtTokenProvier๋ฅผ ํ†ตํ•ด์„œ ์ธ์ฆ ํ›„ SecurityContextHolder๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
@Override 
protected void configure(HttpSecurity http) throws Exception { 
    http
    .csrf().disable()
    .httpBasic().disable()
    .authorizeRequests()
    .antMatchers("/login").permitAll()
    .antMatchers("/exists/**").permitAll() //์ค‘๋ณต ์—ฌ๋ถ€ ๊ฒ€์‚ฌ
    .antMatchers("/checkUser").hasRole("USER")
    .antMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().permitAll()
    .and() 
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and() 
    .formLogin()
        .disable();
    http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}

 

๐Ÿ“Œ jwtToken ์ƒ์„ฑ

jwt Token์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

  • .setIssuedAt(now)
  • .setExpiration(new Date(now.getTime() + Duration.ofMinutes(30).toMillis()))
    • ํ† ํฐ ๋ฐœํ–‰ ์ผ์ž์™€ ๋งŒ๋ฃŒ์ผ์ž๋ฅผ ์ง€์ •ํ•ด์ค๋‹ˆ๋‹ค. ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด ์ง€๋‚˜๋ฉด ํ† ํฐ์˜ ๊ถŒํ•œ์ด ์—†์–ด์ง‘๋‹ˆ๋‹ค.
  • .signWith(SignatureAlgorithm.HS256, JwtProperties.getSecretKey())
    • ์„œ๋ช…์˜ ๊ฒฝ์šฐ HS256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ด์šฉํ•˜๊ณ , key๋Š” ์ด์ „์— ์ง€์ •ํ•œ key์˜ byte๊ฐ’์„ ์ž…๋ ฅํ•ด ์ด์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
public static String makeJwtToken(UserVO user) {
    Date now = new Date();
    
    return Jwts.builder()
            .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
            .setIssuer(JwtProperties.getIssuer())
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + Duration.ofMinutes(30).toMillis()))
            .claim("id", user.getUsername())
            .claim("auth", user.getAuth())
            .signWith(SignatureAlgorithm.HS256, JwtProperties.getSecretKey())
            .compact();
}

 

 

๐Ÿ“Œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”

BCrypt ํ•ด์‹ฑ ํ•จ์ˆ˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ด๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์ œ์ถœํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ DB์— ์ €์žฅ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ์ผ์น˜ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™” ํ•จ์œผ๋กœ์จ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋…ธ์ถœ๋˜๋”๋ผ๋„ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ต๋„๋ก ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

@Bean //๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”
public BCryptPasswordEncoder bCryptPasswordEncoder() 
{ 
    return new BCryptPasswordEncoder(); 
}

 

Comments