Bibi's DevLog ๐ค๐
[Spring] ๊ตฌ๊ธ OAuth ๊ตฌํํ๊ธฐ (+JWT) ๋ณธ๋ฌธ
[Spring] ๊ตฌ๊ธ OAuth ๊ตฌํํ๊ธฐ (+JWT)
๋น๋น bibi 2021. 6. 10. 00:01[Spring] ๊ตฌ๊ธ OAuth ๊ตฌํํ๊ธฐ (+JWT)
์ถ์ฒ
https://preamtree.tistory.com/167
https://withseungryu.tistory.com/116
๊ตฌ๊ธ ๊ณต์๋ฌธ์ - OAuth2.0์ผ๋ก ๊ตฌ๊ธAPI ์ก์ธ์คํ๊ธฐ
- ํนํ "์๋ฒ์ธก ์น ์ฑ์ฉ OAuth 2.0" ์ฐธ๊ณ
์ ์ฒด flow
/login
์ผ๋ก ์ ์ ์ ๊ตฌ๊ธ OAuth2.0์ ์ด์ฉํด ์ฌ์ฉ์ ์ ๋ณด(์น์ธ)์ ์์ฒญํจ. ์น์ธ๋๋ฉด ๋ฆฌ๋๋ ์ URI๋ก ์ด๋. ์ด ๋ ๊ตฌ๊ธ๋ก๋ถํฐcode
๋ฅผ ๋ฐ๊ธ๋ฐ์.- ๊ตฌ๊ธ API ์ฝ์ - API ๋ฐ ์๋น์ค - ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด - ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด ๋ง๋ค๊ธฐ - OAuth ํด๋ผ์ด์ธํธ ID - ์ฑ ์ ํ ์ ํ - ์ด๋ฆ ์ค์ - ์น์ธ๋ ๋ฆฌ๋๋ ์
URI ์ง์ . โ ์ด ๋ ๋ฐ๊ธ๋๋ ํด๋ผ์ด์ธํธID(Client Id), ํด๋ผ์ด์ธํธ ๋ณด์๋น๋ฐ(Client Secret)์ ์ ์ ์ฅํด ๋์ด์ผ ํ๋ฉฐ, ๋
ธ์ถ๋์ด์ ์ ๋จ.
๋ฆฌ๋๋ ์ URI = redirect url. ๊ตฌ๊ธ ์ธ์ฆ ํ ๋ฆฌ๋ค์ด๋ ์ ๋ ์ฃผ์๋ฅผ ์ ๋ ฅํ๋ฉด ๋จ - ๋ฆฌ๋๋ ์
๋ ํ์ด์ง์์
code
์ Client Id, Client Secret ๋ฑ์ ์ด์ฉํด POST์์ฒญ์ ๋ง๋ค์ด ์ ์กํ๋ฉฐ, ์ด ๊ฒฐ๊ณผ๊ฐ์ResponseEntity
๋ก ๋ฐ์. ๊ทธ๋ฆฌ๊ณResponseEntity
์ body๋ก๋ถํฐ OAuth ์ก์ธ์คํ ํฐ์ด ์๋์ง ํ์ธ ํ ๊ฐ์ ธ์ดํ์์ ๋ฐ๋ผ ์ ์ ์ ์ก์ธ์คํ ํฐ์ DB์ ์ผ์์ ์ผ๋ก ์ ์ฅ - ๊ตฌ๊ธ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด, ์ก์ธ์คํ ํฐ์ ์ด์ฉํด GET์์ฒญ์ ๋ง๋ค์ด ๋ณด๋. Http Request ํค๋์ ์ก์ธ์คํ ํฐ์ ๋ฃ์ด ์์ฒญํ๋ฉฐ, ์ด ๊ฒฐ๊ณผ๊ฐ์
ResponseEntity
๋ก ๋ฐ์. ๊ทธ๋ฆฌ๊ณResponseEntity
์ body๋ก๋ถํฐ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ด - ๋ง์ฝ ํด๋น ์ ์ ๊ฐ ๊ฐ์ ๋ ์ ์๋ ์ ์ ๋ผ๋ฉด - ์ก์ธ์คํ ํฐ์ ๋ด์ ํด๋น ์ ์ ๋ฅผ ํ์๊ฐ์ ์ํด / ์ด๋ฏธ ๊ฐ์ ๋ ์ ์๋ ์ ์ ๋ผ๋ฉด - ๋ก๊ทธ์ธ(= ์ ์ ์ ๋ณด๊ฐ ๋ด๊ธด JWTํ ํฐ ๋ฐ๊ธ)
- ๋ฆฌ๋๋ ์ URI์ ๋ํ ์์ฒญ์ ์๋ต์ผ๋ก JWTํ ํฐ์ ๋ด์ ๋ฐํ
build.gradle
์์กด์ฑ์ implementation 'io.jsonwebtoken:jjwt:0.9.1'
๋ฅผ ์ถ๊ฐํด์ผ ํจ
์๋๋ ๋ด๊ฐ ์ถ๊ฐํ ์์กด์ฑ ์ฌํญ๋ค..
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
compile 'javax.xml.bind:jaxb-api:2.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
UserController
@Controller
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class UserController {
private static final String ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
private static final String CLIENT_ID = "";
private static final String REDIRECT_URI = "";
private static final String RESPONSE_TYPE = "code";
private static final String SCOPE = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile";
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/login")
public String login() {
return "redirect:" + ENDPOINT + "?client_id=" + CLIENT_ID + "&redirect_uri=" + REDIRECT_URI
+ "&response_type=" + RESPONSE_TYPE + "&scope=" + SCOPE;
}
@GetMapping("/oauth/google/callback")
public ResponseEntity<TokenResponse> oauthLogin(String code) {
String token = userService.oauthLogin(code);
return new ResponseEntity(new TokenResponse(token, "bearer"), HttpStatus.OK);
}
}
UserService
@Service
public class UserService {
private Logger logger = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
private final OAuthService oauthService;
private final JwtTokenProvider jwtTokenProvider;
public UserService(UserRepository userRepository, OAuthService oauthService, JwtTokenProvider jwtTokenProvider) {
this.userRepository = userRepository;
this.oauthService = oauthService;
this.jwtTokenProvider = jwtTokenProvider;
}
public String oauthLogin(String code) {
ResponseEntity<String> accessTokenResponse = oauthService.createPostRequest(code);
OAuthToken oAuthToken = oauthService.getAccessToken(accessTokenResponse);
logger.info("Access Token: {}", oAuthToken.getAccessToken());
ResponseEntity<String> userInfoResponse = oauthService.createGetRequest(oAuthToken);
GoogleUser googleUser = oauthService.getUserInfo(userInfoResponse);
logger.info("Google User Name: {}", googleUser.getName());
if (!isJoinedUser(googleUser)) {
signUp(googleUser, oAuthToken);
}
User user = userRepository.findByEmail(googleUser.getEmail()).orElseThrow(UserNotFoundException::new);
return jwtTokenProvider.createToken(user.getId());
}
private boolean isJoinedUser(GoogleUser googleUser) {
Optional<User> users = userRepository.findByEmail(googleUser.getEmail());
logger.info("Joined User: {}", users);
return users.isPresent();
}
private void signUp(GoogleUser googleUser, OAuthToken oAuthToken) {
User user = googleUser.toUser(oAuthToken.getAccessToken());
userRepository.insert(user);
}
}
OAuthService
@Service
public class OAuthService {
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private static final String CLIENT_ID = "";
private static final String CLIENT_SECRET = "";
private static final String REDIRECT_URI = "";
private static final String GRANT_TYPE = "authorization_code";
public OAuthService(RestTemplate restTemplate) {
this.objectMapper = new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
this.restTemplate = restTemplate;
}
public ResponseEntity<String> createPostRequest(String code) {
String url = "https://oauth2.googleapis.com/token";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("client_id", CLIENT_ID);
params.add("client_secret", CLIENT_SECRET);
params.add("redirect_uri", REDIRECT_URI);
params.add("grant_type", GRANT_TYPE);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/x-www-form-urlencoded");
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
return restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
}
public OAuthToken getAccessToken(ResponseEntity<String> response) {
OAuthToken oAuthToken = null;
try {
oAuthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return oAuthToken;
}
public ResponseEntity<String> createGetRequest(OAuthToken oAuthToken) {
String url = "https://www.googleapis.com/oauth2/v1/userinfo";
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + oAuthToken.getAccessToken());
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity(headers);
return restTemplate.exchange(url, HttpMethod.GET, request, String.class);
}
public GoogleUser getUserInfo(ResponseEntity<String> userInfoResponse) {
GoogleUser googleUser = null;
try {
googleUser = objectMapper.readValue(userInfoResponse.getBody(), GoogleUser.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return googleUser;
}
}
- RestTemplate : HTTP get, post ์์ฒญ์ ๋ ๋ฆด ๋ ํ์ํ๋ค.
- ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํด ์ฌ์ฉ
-
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())) .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8"))) .build(); } }
- ObjectMapper : JSON์ผ๋ก ๋ฐํ๋๋ ๋ฆฌํด๊ฐ์ ๊ฐ์ฒด๋ก ๋ง๋ค์ด ์ฃผ๊ธฐ ์ํด ์ฌ์ฉ
- MultiValueMap :
JwtTokenProvider
@Component
public class JwtTokenProvider {
private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
private String secretKey;
private long validityInMilliseconds;
public JwtTokenProvider(@Value("&{security.jwt.token.secret-key}") String secretKey, @Value("${security.jwt.token.expire-length}") long validityInMilliseconds) {
this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
this.validityInMilliseconds = validityInMilliseconds;
}
// ํ ํฐ ์์ฑ
public String createToken(Long id) {
Claims claims = Jwts.claims().setSubject(String.valueOf(id));
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds); // ์ ํจ๊ธฐ๊ฐ ๊ณ์ฐ (์ง๊ธ์ผ๋ก๋ถํฐ + ์ ํจ์๊ฐ)
logger.info("now: {}", now);
logger.info("validity: {}", validity);
return Jwts.builder()
.setClaims(claims) // sub ์ค์
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey) // ์ํธํ๋ฐฉ์?
.compact();
}
// ํ ํฐ์์ ๊ฐ ์ถ์ถ
public Long getSubject(String token) {
return Long.valueOf(Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject());
}
// ์ ํจํ ํ ํฐ์ธ์ง ํ์ธ
public boolean validateToken(String token) {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
}
}
JWTํ ํฐ๊ณผ ๊ด๋ จ๋ ์์ ์ ๋ด๋นํ๋ ํด๋์ค
- ํ ํฐ ์์ฑ
- ํ ํฐ์์ (์ ์ )์ ๋ณด ์ถ์ถ
- ํ ํฐ ์ ํจ์ฑ ํ์ธ
โ ์์ฑ์์ &{security.jwt.token.secret-key}
, ${security.jwt.token.expire-length}
๋ ํ๋ก์ ํธ์ application.properties
์์ ์ค์ ๊ฐ๋ฅํ๋ค.
application.properties
-
security.jwt.token.secret-key=Vu6WXrg3t9 security.jwt.token.expire-length=3600000
secret-key
๋ ์ํ๋ ์์์ ๊ฐ์ ๋ฃ์ผ๋ฉด ๋๋ค. random key generator ๋ฑ์ผ๋ก ๊ตฌ๊ธ๋งexpire-length
๋ ms(๋ฐ๋ฆฌ์ธ์ปจ๋)๋จ์์ด๋ค. ํ ํฐ์ ์ ํจ๊ธฐ๊ฐ์ ์ค์ ํ๋ ๋ถ๋ถ์
AppConfig
@Configuration
public class AppConfig implements WebMvcConfigurer {
private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
private final BearerAuthInterceptor bearerAuthInterceptor;
public AppConfig(BearerAuthInterceptor bearerAuthInterceptor) { // ์ธํฐ์
ํฐ ๊ตฌํ์ฒด๋ฅผ ๋ง๋ค์ด ๋ฑ๋กํจ
this.bearerAuthInterceptor = bearerAuthInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
logger.info(">>> ์ธํฐ์
ํฐ ๋ฑ๋ก");
registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/api/booking")
.addPathPatterns("/api/booking/{bookingId}")
.addPathPatterns("/api/rooms/{userId}/wish/{roomId}");
}
}
(JWT ํ ํฐ ๊ฒ์ฆ์ ์ํ) ์ธํฐ์ ํฐ๋ฅผ ๋ฑ๋กํ๋ ํด๋์ค
addInterceptors()
์ ๋ฑ๋ก๋ URL์์๋ง ํ ํฐ์ ๊ฒ์ฆํ๊ฒ ๋จ
BearerAuthInterceptor
AppConfig์ ๋ฑ๋กํ ์ธํฐ์ ํฐ์ ๊ตฌํ์ฒด
@Component
public class BearerAuthInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(BearerAuthInterceptor.class);
private AuthorizationExtractor authorizationExtractor;
private JwtTokenProvider jwtTokenProvider;
public BearerAuthInterceptor(AuthorizationExtractor authorizationExtractor, JwtTokenProvider jwtTokenProvider) {
this.authorizationExtractor = authorizationExtractor;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
logger.info(">>> interceptor.preHandle ํธ์ถ");
String token = authorizationExtractor.extract(request, "Bearer");
if (token.isEmpty()) {
throw new TokenEmptyException();
}
if (!jwtTokenProvider.validateToken(token)) {
throw new IllegalArgumentException("์ ํจํ์ง ์์ ํ ํฐ");
}
Long id = jwtTokenProvider.getSubject(token);
request.setAttribute("id", id);
return true;
}
}
AuthorizationExtractor
HTTP ์์ฒญ์ผ๋ก๋ถํฐ JWTํ ํฐ์ ์ถ์ถํ๋ ํด๋์ค
@Component
public class AuthorizationExtractor {
public static final String AUTHORIZATION = "Authorization";
public static final String ACCESS_TOKEN_TYPE = AuthorizationExtractor.class.getSimpleName();
public String extract(HttpServletRequest request, String type) {
Enumeration<String> headers = request.getHeaders(AUTHORIZATION);
while (headers.hasMoreElements()) {
String value = headers.nextElement();
if (value.toLowerCase().startsWith(type.toLowerCase())) {
return value.substring(type.length()).trim();
}
}
return Strings.EMPTY;
}
}
OAuthToken
์ก์ธ์คํ ํฐ ์ ๋ณด๋ฅผ ๋ด๊ธฐ ์ํ ํด๋์ค
public class OAuthToken {
private String accessToken;
private String expiresIn;
private String idToken;
private String refreshToken;
private String scope;
private String tokenType;
public OAuthToken() {
}
public OAuthToken(String accessToken, String expiresIn, String idToken, String refreshToken, String scope, String tokenType) {
this.accessToken = accessToken;
this.expiresIn = expiresIn;
this.idToken = idToken;
this.refreshToken = refreshToken;
this.scope = scope;
this.tokenType = tokenType;
}
public String getAccessToken() {
return accessToken;
}
public String getExpiresIn() {
return expiresIn;
}
public String getIdToken() {
return idToken;
}
public String getRefreshToken() {
return refreshToken;
}
public String getScope() {
return scope;
}
public String getTokenType() {
return tokenType;
}
}
GoogleUser
์ก์ธ์คํ ํฐ์ ์ฌ์ฉํด ๋ฐ์ ์จ ๊ตฌ๊ธ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด๊ธฐ ์ํ ํด๋์ค
public class GoogleUser {
public String id;
public String email;
public Boolean verifiedEmail;
public String name;
public String givenName;
public String familyName;
public String picture;
public String locale;
public GoogleUser() {
}
public GoogleUser(String id, String email, Boolean verifiedEmail, String name, String givenName, String familyName, String picture, String locale) {
this.id = id;
this.email = email;
this.verifiedEmail = verifiedEmail;
this.name = name;
this.givenName = givenName;
this.familyName = familyName;
this.picture = picture;
this.locale = locale;
}
public User toUser(String accessToken) {
return new User(email, name, accessToken);
}
public String getId() {
return id;
}
public String getEmail() {
return email;
}
public Boolean getVerifiedEmail() {
return verifiedEmail;
}
public String getName() {
return name;
}
public String getGivenName() {
return givenName;
}
public String getFamilyName() {
return familyName;
}
public String getPicture() {
return picture;
}
public String getLocale() {
return locale;
}
}
TokenResponse
OAuthํ ํฐ์ ๋ด๊ธฐ ์ํ ํด๋์ค. (OAuthํ ํฐ ์์ ์ก์ธ์คํ ํฐ์ด ๋ค์ด ์๋ค)
public class TokenResponse {
private String accessToken;
private String tokenType;
public TokenResponse(String accessToken, String tokenType) {
this.accessToken = accessToken;
this.tokenType = tokenType;
}
public String getAccessToken() {
return accessToken;
}
public String getTokenType() {
return tokenType;
}
}