728x90
OAuth2 Code Grant
클라이언트 애플리케이션이 사용자 대신 리소스 서버에 접근할 수 있도록 하는 인증 프로토콜이다.
이 방식은 서버 측 애플리케이션에서 주로 사용되며, 클라이언트가 액세스 토큰을 안전하게 획득할 수 있도록 힌다.
동작순서
- Authorization Request (인증 요청):
- 클라이언트 애플리케이션은 사용자를 권한 부여 서버로 리다이렉션한다
- 이 요청에는 response_type=code, client_id, redirect_uri, 그리고 scope가 포함된다
- 사용자는 권한 부여 서버에서 로그인하고, 애플리케이션에 권한을 부여할지 결정한다
- Authorization Response (인증 응답):
- 사용자가 권한을 부여하면, 권한 부여 서버는 클라이언트를 redirect_uri로 리다이렉션하며, authorization code를 포함한 응답을 보낸다
- 클라이언트는 이 authorization code를 수신한다
- Token Request (토큰 요청):
- 클라이언트는 받은 authorization code를 사용하여 권한 부여 서버에 액세스 토큰을 요청한다
- 이 요청에는 grant_type=authorization_code, code, redirect_uri, client_id, 그리고 client_secret가 포함된다
- Token Response (토큰 응답):
- 권한 부여 서버는 요청을 검증한 후, 클라이언트에게 액세스 토큰과 리프레시 토큰을 발급한다
- 클라이언트는 이 토큰을 저장하고, API 요청 시 사용한다
- Accessing Resources (자원 접근):
- 클라이언트 애플리케이션은 받은 액세스 토큰을 사용하여 리소스 서버에 요청을 보낸다
- 리소스 서버는 액세스 토큰을 검증하고, 요청된 리소스를 반환한다
동작원리
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'kr.bit'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
// runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}
tasks.named('test') {
useJUnitPlatform()
}
Controller
package kr.bit.oauth2jwt.controller;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Configuration
public class MainController {
@GetMapping("/")
@ResponseBody
public String mainAPI() {
return "main route";
}
}
package kr.bit.oauth2jwt.controller;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Configuration
public class MyController {
@GetMapping("/my")
@ResponseBody
public String myAPI() {
return "my route";
}
}
SecurityConfig
package kr.bit.oauth2jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//HTTP Basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
//oauth2
http
.oauth2Login(Customizer.withDefaults());
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
//세션 설정 : STATELESS
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
application.properties
#NAVER
#registration
spring.security.oauth2.client.registration.naver.client-name=naver
spring.security.oauth2.client.registration.naver.client-id=아이디
spring.security.oauth2.client.registration.naver.client-secret=비밀번호
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email
#provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
#GOOGLE
#registration
spring.security.oauth2.client.registration.google.client-name=google
spring.security.oauth2.client.registration.google.client-id=아이디
spring.security.oauth2.client.registration.google.client-secret=비밀번호
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google
spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.google.scope=profile,email
네이버 소셜 로그인 요청 경로
GET : /oauth2/authorization/naver
패키지 구성
OAuth2UserService
package kr.bit.oauth2jwt.service;
import kr.bit.oauth2jwt.dto.*;
import kr.bit.oauth2jwt.entity.UserEntity;
import kr.bit.oauth2jwt.repository.UserRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User.getAttributes());
//어느 소셜 아이디인지 구분
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("google")) {
oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
} else {
return null;
}
String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
UserEntity existData = userRepository.findByUsername(username);
if (existData == null) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setEmail(oAuth2Response.getEmail());
userEntity.setName(oAuth2Response.getName());
userEntity.setRole("ROLE_USER");
userRepository.save(userEntity);
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setName(oAuth2Response.getName());
userDTO.setRole("ROLE_USER");
return new CustomOAuth2User(userDTO);
} else {
existData.setEmail(oAuth2Response.getEmail());
existData.setName(oAuth2Response.getName());
userRepository.save(existData);
UserDTO userDTO = new UserDTO();
userDTO.setUsername(existData.getUsername());
userDTO.setName(existData.getName());
userDTO.setRole(existData.getRole());
return new CustomOAuth2User(userDTO);
}
}
}
UserDTO
package kr.bit.oauth2jwt.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserDTO {
private String role;
private String name;
private String username;
}
CustomOAuth2User
package kr.bit.oauth2jwt.dto;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class CustomOAuth2User implements OAuth2User {
private final UserDTO userDTO;
public CustomOAuth2User( UserDTO userDTO) {
this.userDTO = userDTO;
}
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userDTO.getRole();
}
});
return collection;
}
@Override
public String getName() {
return userDTO.getName();
}
public String getUsername() {
return userDTO.getUsername();
}
}
UserEntity
package kr.bit.oauth2jwt.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String name;
private String email;
private String role;
}
나머지 클래스는 이 전에 포스팅한 세션방식과 동일함
JWT
application.properties
암호화키 추가
spring.jwt.secret=sksmsrhtnrkehlfrjdidmgkgkslrkanjfdkfdksorkwpdlfwkfskrk
jwt / JWTUtil(0.12.3)
package kr.bit.oauth2jwt.jwt;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createJwt(String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
로그시 성공시 JWT 발급
oauth2 / CustomSuccessHandler
package kr.bit.oauth2jwt.oauth2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.oauth2jwt.dto.CustomOAuth2User;
import kr.bit.oauth2jwt.jwt.JWTUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JWTUtil jwtUtil;
public CustomSuccessHandler(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
String username = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String token = jwtUtil.createJwt(username, role, 60*60*60L);
response.addCookie(createCookie("Authorization", token));
response.sendRedirect("http://localhost:3000/");
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(60*60*60);
//cookie.setSecure(ture);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
}
SecurityConfig
package kr.bit.oauth2jwt.config;
import kr.bit.oauth2jwt.jwt.JWTUtil;
import kr.bit.oauth2jwt.oauth2.CustomSuccessHandler;
import kr.bit.oauth2jwt.service.CustomOAuth2UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JWTUtil jwtUtil) {
this.customOAuth2UserService = customOAuth2UserService;
this.customSuccessHandler = customSuccessHandler;
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//HTTP Basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
//oauth2
http
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.successHandler(customSuccessHandler));
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
//세션 설정 : STATELESS
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
검증
JWT Filter
package kr.bit.oauth2jwt.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kr.bit.oauth2jwt.dto.CustomOAuth2User;
import kr.bit.oauth2jwt.dto.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
if (cookie.getName().equals("Authorization")) {
authorization = cookie.getValue();
}
}
//Authorization 헤더 검증
if (authorization == null) {
System.out.println("token is null");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰
String token = authorization;
//토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token is expired");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
//userDTO를 생성하여 값 set
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setRole(role);
//UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
// 스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
SecurityConfig
코드 추가
http
.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
CORS
SecurityConfig
코드 추가
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
return configuration;
}
}));
config / CorsMvcConfig
package kr.bit.oauth2jwt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.exposedHeaders("Set-Cookie")
.allowedOrigins("http://localhost:3000");
}
}
무한리다이렉트 에러 해결
//JWTFilter 추가
http
.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);
// //JWTFilter 추가
// http
// .addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
728x90
'Frameworks > Spring(개인)' 카테고리의 다른 글
[Spring 개인] 스프링 컨테이너 / 스프링 빈 조회 (0) | 2024.06.07 |
---|---|
[Spring 개인] Spring Security JWT (Access/Refresh Token) (1) | 2024.05.30 |
[Spring 개인] Spring Security OAuth2.0(세션 클라이언트) (0) | 2024.05.28 |
[Spring 개인] Spring Security OAuth2.0(네이버, 구글, 카카오) II (0) | 2024.05.28 |
[Spring 개인] Spring Security OAuth2.0(네이버, 구글, 카카오) (1) | 2024.05.28 |