Spring-Security OAuth2

jm·2023년 2월 2일
1

Login

목록 보기
1/2

📌 목표

  • spring-security로 OAuth2로그인을 하여 사용자를 인증하고 JWT토큰으로 관리. 나아가 custom handler가 필요한 kakao login으로 예제 구현
    🛠tech stack
    • Spring-Boot: 2.7.7
      Vue3
      JWT

      📂 디렉터리 구조

      ├─main
         ├─java
         │  └─com
         │      └─Project
         │          │  Project.java
         │          │
         │          ├─domain
         │          │  └─user
         │          │      ├─controller
         │          │      │      MemberController.java
         │          │      │      TokenController.java
         │          │      │
         │          │      ├─dto
         │          │      │      MemberAccessDto.java
         │          │      │
         │          │      ├─entity
         │          │      │      Member.java
         │          │      │
         │          │      ├─ouath
         │          │      │      OAuth2SuccessHandler.java
         │          │      │      Token.java
         │          │      │
         │          │      ├─repository
         │          │      │
         │          │      └─service
         │          │              CustomOAuth2UserService.java
         │          │              MemberService.java
         │          │              OAuth2Attribute.java
         │          │              TokenService.java
         │          │
         │          └─global
         │              ├─config
         │              │      RedisConfig.java
         │              │      SecurityConfig.java
         │              │
         │              ├─error
         │              │  │  ErrorCode.java
         │              │  │  ErrorResponse.java
         │              │  │  GlobalExceptionHandler.java
         │              │  │
         │              │  └─exception
         │              │          AccessDeniedException.java
         │              │          NotFoundException.java
         │              │
         │              ├─filter
         │              │      JwtAuthFilter.java
         │              │
         │              ├─response
         │                      BaseResponse.java
         │
         │
         │
         │
         └─resources
                 application-env.yml
                 application.yml

⏩ 순서

1. Spring-Security

2. 커스텀 메서드들

3. Kakao login 예제 + CustomHandler


🕛 Spring-Security

💡 Spring Security란
인증 / 인가 및 보호 기능을 제공하는 프레임워크. Spring 기반 애플리케이션을 보호하기 위한 표준. 다양한 보안 솔루션을 제공한다.

  • 다양한 인증방식 HTTP, Digest, OAuth2 등을 지원, CSRF와 같은 웹 공격 방어, SST/TLS 기능 제공
    Srping Security 5.x 이상의 버전에서는 Java 8부터 지원

Spring-Security 5.4V 이상부터는 WebSecurityConfigurerAdapter 를 지원하지않고
SecurityFilterChain을 사용하여 Bean에 등록하는 것을 권장한다.

spring.io/blog

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }
}

아래와 같이 oauth2와 함께 커스텀해서 사용할 수 있다.
소셜로그인만 구현했을 경우.

@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .httpBasic().disable()  // Http basic Auth  기반으로 로그인 인증창이 뜸.  disable 시에 인증창 뜨지 않음.
        .csrf().disable()       // rest api이므로 csrf 보안이 필요없으므로 disable처리.
        .sessionManagement().sessionCreationPolicy(
            SessionCreationPolicy.STATELESS)// jwt token으로 인증하므로 stateless 하도록 처리.
        .and()
        .authorizeRequests() // 인증권한 페이지 설정
            .antMatchers(HttpMethod.GET, "/api/~~").permitAll() //인증 무시하기
            .anyRequest().authenticated() //인증 하기
        .and()
        	//Jwt토큰을 체크하는 부분(커스텀 필터) 추가 OAuth2login을 진행한 이후
            //UsernamePasswordAuthenticationFilter.class
            .addFilterAfter(new JwtAuthFilter(tokenService, memberRepository), 
                   OAuth2LoginAuthenticationFilter.class)
        .oauth2Login()
        .loginPage("http://localhost:8080") //login이 필요한 경우 
        .successHandler(successHandler) //로그인이 정상적으로 성공했을 때 필요한 핸들러 ex) 회원가입, 토큰 생성
        .userInfoEndpoint().userService(customOAuth2UserService); //유저 정보를 가져왔을 때 successHandler에서 사용하기 위한 추가적인 구현 부분
//        .failureHandler(oAuth2AuthenticationFailureHandler);
return http.build();


2023-05-25 추가

🕒 커스텀 메소드 소개

JwtAuthFilter : JWT 작성글에서 자세히 다룰 예정
successHandler : social login이 정상적으로 인증된 후 추가할 로직 처리 (ex. 회원가입 등..)
customOAuth2UserService : 유저 정보(Authentication)를 처리하고 저장할 로직

successHandler

@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
	private final TokenService tokenService;
    private final MemberRepository memberRepository;
    private final RedisTemplate redisTemplate;
    
    @Value("${LOGIN_SUCCESS_URL}") //client의 login 성공 페이지
    private String loginSuccessUrl;

    @Value("${jwt.refresh-token.expire-length}") //refresh Token 사용할 경우
    private long refreshTokenExpiretime;
	
    /**
     *
     * @param request the request which caused the successful authentication
     * @param response the response
     * @param authentication 인증 프로세스 중에 생성된 Authentication 객체 (CustomUserService 에서 생성된다)
     * the authentication process.
     * @throws IOException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException, ServletException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        Map<String, Object> attributes = oAuth2User.getAttributes();

        Member member = memberRepository.findByEmail((String) attributes.get("email"));

        if (member == null) { //회원 가입.
            member = Member.builder()
                .email((String) attributes.get("email"))
                .nickname((String) attributes.get("nickname"))
                .build();

            Member save = memberRepository.save(member);
        }

        if (member.getDelFlag() == 1) { //재 가입 불가능 throw 던지기, Filter ExceptionHandler 작성

            return;
        }

        Token token = tokenService.generateToken(member.getId(), "USER");

        Cookie cookie = new Cookie("refresh-token", token.getRefreshToken());
        // expires in 7 days
        cookie.setMaxAge(60 * 60 * 24 * 14);
        // optional properties
        cookie.setSecure(true);
        cookie.setHttpOnly(true);
        cookie.setPath("/api");

        // add cookie to response
        response.addCookie(cookie);
        
        // + refresh Token 저장 로직 추가
        
        //client로 token 전달
        response.sendRedirect(loginSuccessUrl + token.getToken());
    }

}

🕕 Kakao login + Custom Handler

Spring Security Ouath2에서는 google, facebook 등 Provider를 기본 제공해주는 기능이 있다.
하지만 오늘은 kakao Login 예제를 통해 Provider 설정까지 해볼 예정이다.

순서

  1. Application.yml 설정
  2. OAuth2Attribute 설정
  3. userInfoEndpoint에 설정할 CustomUserService

Application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_CLIENT_ID} #REST API 키
            redirect-uri: ${KAKAO_REDIRECT_URL} #설정한 redirect 주소
            client-authentication-method: POST #변경 X
            client-secret: ${KAKAO_CLIENT_SECRET} #보안 설정 시 Client Secret
            authorization-grant-type: authorization_code #변경 X
            scope: #동의항목을 통해 가져올 사용자 정보
              - profile_nickname
              - account_email
            client_name: kakao
        provider: #인증 및 정보를 가져오기 위한 provider 설정
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

OAuth2Attribute.java

@Builder(access = AccessLevel.PRIVATE)
class OAuth2Attribute {

    private Map<String, Object> attributes;
    private String attributeKey;
    private String email;
    private String nickname;
	
	/**
     *
     * @param provider client 식별 name ex) kakao
     * @param attributeKey attribute 식별 값
     * @param attributes 전달 받은 정보
     * @return
     */
    static OAuth2Attribute of(String provider, String attributeKey,
        Map<String, Object> attributes) {
        switch (provider) {
            case "kakao": //다른 소셜로그인 추가
                return ofKakao(attributeKey, attributes);
            default:
                throw new RuntimeException();
        }
    }

    /**
     *
     * @param attributeKey OAuth2 유저를 식별하기 위한 Key
     * @param attributes OAuth2 유저 정보. 소셜 마다 attributes의 구조가 다를 수 있음
     * @return 필요한 유저 정보
     */
    private static OAuth2Attribute ofKakao(String attributeKey,
        Map<String, Object> attributes) {
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");


        return OAuth2Attribute.builder()
            .nickname((String) kakaoProfile.get("nickname"))
            .email((String) kakaoAccount.get("email"))
            .profile((String) kakaoProfile.get("profile_image_url"))
            .attributes(kakaoAccount)
            .attributeKey(attributeKey)
            .build();
    }

    /**
     *
     * @return User 정보가 들어있는 Map
     */
    Map<String, Object> convertToMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("key", attributeKey);
        map.put("nickname", nickname);
        map.put("email", email);
        map.put("profile", profile);
        map.put("gender", gender);
        map.put("age", age);

        return map;
    }
}

CustomUserService

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration()
            .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        //Success Handler가 사용할 수 있도록 등록
        OAuth2Attribute oAuth2Attribute =
            OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        var memberAttribute = oAuth2Attribute.convertToMap();

        return new DefaultOAuth2User(
            Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
            memberAttribute, "email");
    }
}

💡 OAuth2UserService 인터페이스란 번역: 인터페이스의 구현체는 클라이언트에게 부여된 액세스 토큰을 사용하여 UserInfo 엔드포인트에서 엔드 유저 (리소스 소유자)의 사용자 속성을 가져오고, OAuth2User 형태의 인증된 주체(AuthenticatedPrincipal)를 반환하는 역할을 담당합니다.

💡 DefaultOAuth2User 란
    DefaultOAuth2User.java 에 적혀있는 설명
OAuth2User의 기본 구현입니다.
사용자 속성 이름은 제공자 간에 표준화되어 있지 않으므로 사용자의 "name" 속성을 위한 키를 생성자 중 하나에 제공해야 합니다. 이 키는 getAttributes()를 통해 Principal(사용자)의 "name"에 액세스하고 getName()에서 반환하는 데 사용됩니다.

DefaultOAuth2User는 OAuth2User 인터페이스의 기본 구현체입니다. 이를 사용하는 이유는 다음과 같습니다:
표준화되지 않은 사용자 속성 이름: OAuth2 공급자마다 사용자 속성의 이름이 일관되지 않을 수 있습니다. 예를 들어, Google과 Facebook은 사용자의 이름 속성을 각각 "name"과 "displayName"으로 제공할 수 있습니다. DefaultOAuth2User는 이러한 다양한 속성 이름을 처리하고, 사용자의 이름 속성에 액세스하기 위해 사용자가 지정한 키를 사용할 수 있습니다.

profile
ㅎㅎ

0개의 댓글

관련 채용 정보