[security] SpringBoot + JWT + OAuth 적용하기

Coastby·2023년 1월 19일
0

Daengnyang 프로젝트

목록 보기
5/12
post-custom-banner

💡 카카오는 유저정보 제한이 심하여 보류하였다.

Resource Owner리소스 소유자 또는 사용자. 보호된 자원에 접근할 수 있는 자격을 부여해 주는 주체. OAuth2 프로토콜 흐름에서 클라이언트를 인증(Authorize)하는 역할을 수행한다. 인증이 완료되면 권한 획득 자격(Authorization Grant)을 클라이언트에게 부여한다. 개념적으로는 리소스 소유자가 자격을 부여하는 것이지만 일반적으로 권한 서버가 리소스 소유자와 클라이언트 사이에서 중개 역할을 수행하게 된다.
Client보호된 자원을 사용하려고 접근 요청을 하는 애플리케이션이다.
Resource Server사용자의 보호된 자원을 호스팅하는 서버이다.
Authorization Server권한 서버. 인증/인가를 수행하는 서버로 클라이언트의 접근 자격을 확인하고 Access Token을 발급하여 권한을 부여하는 역할을 수행한다.

https://blog.naver.com/mds_datasecurity/222182943542

추가할 것

  • OAuth회원, 회원가입회원 통합시키는 구간 필요
  • UserLoginService 합쳐도 될 듯 → repository Di로 바꿈
  • OAuth2AuthenticationSuccessHandler 62줄, 일단 다 ROLE_USER로 등록
  • interceptor를 이용한 인가처리 구현 (참고)
  • CustomOAuth2Service 에서 UserProfile userProfile = OAuthAttributes.*extract*(registrationId, oAuth2User);에 attribute만 넘겨도 되도록 리팩토링 필요
localhost:8080/oauth2/authorization/google
localhost:8080/oauth2/authorization/naver

관련 클래스 구조


├── configuration
│   └── SecurityConfiguration.java
├── domain
│   └── User.java
├── dto
│   ├── Response.java
│   └── user
│       ├── OAuthAttributes.java
│       ├── UserProfile.java
│       ├── UserLoginResponse.java
│       └── UserRole.java
├── repository
│   └── UserRepository.java
├── security
│   ├── CookieAuthorizationRequestRepository.java
│   ├── CustomOAuth2Service.java
│   ├── JwtExceptionFilter.java
│   ├── JwtFilter.java
│   └── OAuth2AuthenticationSuccessHandler.java
├── service
│   ├── UserDetailsServiceImpl.java
│   └── UserService.java
└── util
    ├── CookieUtil.java
    └── JwtUtil.java

1. dependency 추가

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.0.1'

2. Resource server에 OAuth 서비스 등록 (참고)

  1. 구글
    1. https://console.cloud.google.com/home/dashboard 에서 새 프로젝트 만들기 - 프로젝트 이름 입력 - 프로젝트 만들기
    2. 생성된 프로젝트에서 API 및 서비스 클릭
  2. naver https://developers.naver.com/apps/#/register
  3. 카카오 : https://docs.kakaoi.ai/kakao_i_agent/instance/application/

3. application.yml 추가

spring:
	security:
    oauth2:
      client:
        registration:
          google:
            client-id: 482160302560-b4fpna955hs5mgjjpns9rsf12ffva64u.apps.googleusercontent.com
            client-secret: 비밀키
            scope: profile,email
          # 네이버는 spring security가 기본적을 제공해주지 않기 때문에 github, google과 달리 많은 정보를 적어줘야한다.
          naver:
            client-id: 4mcH9WQY8HRWaiNa7LM6
            client-secret: 비밀키
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            authorization_grant_type: authorization_code
            scope: name,email
            client-name: Naver
          kakao: #app-user-id, access-token, refresh-token
            client-id: 29ed7329b4fd1d7203e6c6315a07ef22
            client-secret: 비밀키
            scope: profile_nickname,account_email
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            authorization-grant-type: authorization_code
            client-name: kakao
            client-authentication-method: POST
        provider:
          naver:
            authorization_uri: https://nid.naver.com/oauth2.0/authorize
            token_uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user_name_attribute: response
          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

#Encoding
server:
  servlet:
    encoding:
      force-response: true

#JWT token
jwt:
  token:
    secret: key
    refresh: "refresh"
#redirect Uri
app:
  oauth2:
    authorizedRedirectUri: "http://localhost:3000/oauth2/redirect"

4. SecurityConfiguration 설정

  • (다른 얘기) 공식문서에서 아래와 같은 경곱 문구가 있어서 SWAGGER를 WebSecurity ignore에서 HttpSecurity .permitAll()로 변경하였다.
  • SecurityConfig에서 CORS를 허용했기 때문에 관련 설정을 추가한다. (참고) http://localhost:3000으로 부터의 요청을 허용한다
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
    private final JwtUtil jwtUtil;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomOAuth2Service customOAuth2Service;
    private final JwtExceptionFilter jwtExceptionFilter;
    private final OAuth2AuthenticationSuccessHandler OAuth2AuthenticationSuccessHandler;

    private final String[] SWAGGER = {
                "/v3/api-docs",
                "/swagger-resources/**", "/configuration/security", "/webjars/**",
                "/swagger-ui.html", "/swagger/**", "/swagger-ui/**"};
    private final String[] UI = {
                "/api/v1/hello/**", "/css/**", "/img/**", "/static/**", "/resources/**", "/", "/index"};
    private final String[] AUTHORIZATION = {
                "/api/v1/users/join", "/api/v1/users/login","/api/v1/users/exception",
                "/oauth2/authorization/**"};
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers(UI).permitAll()
                .antMatchers(SWAGGER).permitAll()
                .antMatchers(AUTHORIZATION).permitAll()
                .antMatchers("/api/v1/posts/my").authenticated()
                .antMatchers(HttpMethod.GET, "/api/v1/posts/**").permitAll()
                .antMatchers("/api/v1/users/*/role/change").access("hasRole('ADMIN')")
                .antMatchers("/api/**").authenticated()
                .anyRequest().hasRole("ADMIN")

                .and()
                .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler)
                .and()
                .exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint)
                .and()
                .oauth2Login()
                .userInfoEndpoint().userService(customOAuth2Service)    //provider로부터 획득한 유저정보를 다룰 service단을 지정한다.
                .and()
                .successHandler(OAuth2AuthenticationSuccessHandler)      //OAuth2 로그인 성공 시 호출한 handler
//                .failureHandler(authenticationFailureHandelr)
                .and()
                .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtExceptionFilter, JwtFilter.class)
                .build();
    }

    @Bean
    public WebMvcConfigurer corsConfigurer(){
        return new WebMvcConfigurer() {
            private final long MAX_AGE_SECS = 3600;

            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:3000")
                        .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .maxAge(MAX_AGE_SECS);
            }
        };
    }
}

5. UserProfile 생성

OAuth에서 가져온 값들을 저장할 class를 생성한다.

Resource server의 Authentication에서 가져온 정보를 담기 위해 OAuth2User를 구현하였다.

@AllArgsConstructor
@Getter
@Builder
public class UserProfile implements OAuth2User{      //Resource Server마다 제공하는 정보가 다르므로 통일시키기 위한 profile
    private String userName; //authentication의 name
    private Collection<? extends GrantedAuthority> authorities;
    private Map<String, Object> attributes; // oauthId, name, email
    public User toUser(){
        return User.builder()
                .oauthId((String) this.attributes.get("oauthId"))
                .userName(this.userName)
                .email((String) this.attributes.get("email"))
                .name((String) this.attributes.get("name"))
                .role(UserRole.ROLE_USER)
                .build();
    }

    @Override
    public Map<String, Object> getAttributes() {
        return this.attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getName() {
        return this.userName;
    }
}

6. OAuthAttribues

OAuth에 따라 attribute을 꺼내어 UserProfile에 저장하기 위한 enum을 생성한다.

@AllArgsConstructor
public enum OAuthAttributes {       //OAuth 서비스에 따라 얻어온 유저 정보의 key값이 다르기 때문에 각각 관리한다.
    GOOGLE("google", (oAuth2User) -> {
        return UserProfile.builder()
                .userName(oAuth2User.getName())
                .authorities(oAuth2User.getAuthorities())
                .attributes(
                        Map.of(
                                "oauthId", String.valueOf(oAuth2User.getAttributes().get("sub")),
                                "name", (String) oAuth2User.getAttributes().get("name"),
                                "email", (String) oAuth2User.getAttributes().get("email")
                        )
                )
                .build();

    }),
    NAVER("naver", (oAuth2User) -> {
        Map<String, String> attributes = oAuth2User.getAttribute("response");
        return UserProfile.builder()
                .userName(attributes.get("id"))
                .authorities(oAuth2User.getAuthorities())
                .attributes(
                        Map.of(
                                "oauthId", attributes.get("id"),
                                "name", attributes.get("name"),
                                "email", attributes.get("email")
                        )
                )
                .build();
    });
    private final String registrationId;
    private final Function<OAuth2User, UserProfile> setUserInfo;

    //OAuth 서비스의 정보를 통해 UserProfile을 얻는다.
    public static UserProfile extract(String registrationId, OAuth2User oAuth2User){
        return Arrays.stream(values())
                .filter(provider -> registrationId.equals(provider.registrationId))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new)
                .setUserInfo.apply(oAuth2User);
    }
}

7. customOAuth2Service

  • access 토큰을 이용해 받아온 사용자 정보를 이용하여 OAuth2User를 반환하는 메서드를 재정의한다.

🚫 dependency 적용이 안 된다!

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client

2.7.5로 버전을 낮추니 패키지를 이용할 수 있었다.

좀 더 근본적인 원인을 찾아서 공식 문서를 보았다. → 더 찾아봐야할 듯 일단 버전 낮춰서 진행

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2Service implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest); //OAuth 서비스(google..)에서 가져온 유저 정보
        log.info("oAuth2User : {}", oAuth2User.toString());
        Map<String, Object> attributes = oAuth2User.getAttributes();   //유저 정보 Map에 담음
        log.info("attribue : {}", attributes.toString());

        String registrationId = userRequest.getClientRegistration().getRegistrationId(); //사용한 OAuth 서비스 이름
        //OAuth 서비스에 따라 유저정보를 공통된 class인 UserProfile 객체로 만들어 준다.
        UserProfile userProfile = OAuthAttributes.extract(registrationId, oAuth2User); /**attribute만 넘기도록 리팩토링 필요**/

        User user = saveOrUpdate(userProfile);      //DB에 저장
        log.info("userName : {}", oAuth2User.getName());
        return userProfile;
    }
    private User saveOrUpdate(UserProfile userProfile){
        String userName = userProfile.getUserName();
        User user = userRepository.findByUserName(userName)
                .map(m -> m.update(userName, (String) userProfile.getAttributes().get("email"))) //OAuth 서비스 유저정보 변경이 있으면 업데이트
                .orElse(userProfile.toUser());          //user가 없으면 새로운 user 생성
        return userRepository.save(user);
    }
}

⌨️ CookieUtil

• 쿠키를 생성, 제거, 직렬화, 역직렬화 하는 클래스

public class CookieUtil {
    public static Optional<Cookie> getCookie(HttpServletRequest request, String name){
        Cookie[] cookies = request.getCookies();;
        if(cookies != null && cookies.length > 0){
            return Arrays.stream(cookies).distinct()
                    .filter(cookie -> cookie.getName().equals(name))
                    .findFirst();
        }
        return Optional.empty();
    }

    public static void addCookie(HttpServletResponse response, String name, String value, int maxAge){
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name){
        Cookie[] cookies = request.getCookies();
        if(cookies != null && cookies.length > 0){
            for (Cookie cookie: cookies) {
                if (cookie.getName().equals(name)) {
                    cookie.setValue("");
                    cookie.setPath("/");
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);
                }
            }
        }
    }
    public static String serialize(Object object){
        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
    }
    public static <T> T deserialize(Cookie cookie, Class<T> cls){
        return cls. cast(SerializationUtils.deserialize(
                Base64.getUrlDecoder().decode(cookie.getValue())));
    }
}

⌨️ CookieAuthorizationRequestRepository

• Provider와의 Authorization 과정에서 Authorization request를 cookie에 저장하기 위한 클래스

  • 인증 요청시 생성된 2개의 쿠키는 인증이 종료될 때 실행되는 successHandler와 failureHandler에서 제거된다
  • 2개의 쿠키 유효시간은 180초로 유효시간 내에 인증요청을 다시하면 만들어졌던 쿠키를 다시 사용한다
@Component
public class CookieAuthorizationRequestRepository implements AuthorizationRequestRepository {
    public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
    public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
    private static final int COOKIE_EXPIRE_SECONDS = 180;
    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        return CookieUtil.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
                .map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class))
                .orElse(null);
    }
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if(authorizationRequest == null){
            removeAuthorizationRequestCookies(request, response);
        }
        CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME,
                CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
        String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
        if(StringUtils.isNotBlank(redirectUriAfterLogin)){
            CookieUtil.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);
        }

    }
    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response){
        CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
    }
    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
        return this.loadAuthorizationRequest(request);
    }
    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
        return this.loadAuthorizationRequest(request);
    }
}

9. JwtUtil 메서드 추가

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtUtil {
    @Value("${jwt.token.secret}")
    private final String secretKey;
    @Value("${jwt.token.refresh}")
    private final String refreshKey;
    private final UserRepository userRepository;
    private final long accessExpiredTimeMs = 1000 * 60 * 60; // 60min
    private final long refreshExpirredTimeMs = 1000 * 60 * 60 * 24 * 7; // 일주일

    //key를 만드는 메서드
    private Key makeKey(){
        return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    //token 생성하는 메서드
    public String generateToken(String userName, UserRole role){
				/**...**/
    }
    public void generateRefreshToken(Authentication authentication, HttpServletResponse response){
        String refreshToken = Jwts.builder()
                .signWith(makeKey())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + refreshExpirredTimeMs))
                .compact();
        saveRefreshToken(authentication, refreshToken);     //refreshToken DB에 저장

        ResponseCookie cookie = ResponseCookie.from(refreshKey, refreshToken)
                .httpOnly(true)
                .secure(true)
                .sameSite("Lax")
                .maxAge(refreshExpirredTimeMs/1000)
                .path("/")
                .build();
        response.addHeader("Set-Cookie", cookie.toString());
    }
		private void saveRefreshToken(Authentication authentication, String refreshToken) {
        UserProfile user = (UserProfile) authentication.getPrincipal();
        String userName = user.getUserName();
        userRepository.updateRefreshToken(userName, refreshToken);
    }
//token에서 claim 추출하는 메서드
//token으로 authentication 꺼내는 메서드

10. OAuth2AuthenticationSuccessHandler

  • OAuth2 로그인 성공시 호출되는 Handler
  • 로그인에 성공하면 JWT를 생성한 다음 authorizedRedirectUri로 client에게 전송한다.
    • → 기존의 로그인 서비스와 동일하게 현재는 JWT를 바디로 전송하도록 구현하였다.

🚫 UserDetails 인터페이스를 활용하기 위해 User에서 구현하였다.

JwtUtils, testcode, controller가 전반적으로 변경되었다..

🚫 OAuth 서비스마다 OAuth2User 구조가 다르다..

google

{
	"Name": [113419156514707185505], 
	"Granted Authorities": [[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]], 
	"User Attributes": [{sub=113419156514707185505, name=조예지, given_name=예지, family_name=조, picture=https://lh3.googleusercontent.com/a/AEdFTp4WdAgnPNep26FCrHdQSxxlg-kSOzWAL0K8K7E=s96-c, [email=whdpwl2@gmail.com](mailto:email=whdpwl2@gmail.com), email_verified=true, locale=ko}]
}
{
	"Name": [{id=-ZJQm9WRI3PcvLa23nY-Mc2BsUp2TJCpHeFCqftCBaY, email=whdpwl2@gmail.com, name=조예지}], 
	"Granted Authorities": [[ROLE_USER]], 
	"User Attributes": [{resultcode=00, message=success, response={id=-ZJQm9WRI3PcvLa23nY-Mc2BsUp2TJCpHeFCqftCBaY, email=whdpwl2@gmail.com, name=조예지}}]
}

kakao —> 보류

⌨️ OAuth2AuthenticationSuccessHandler

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Value("${app.oauth2.authorizedRedirectUri}")
    private String redirectUri;
    private final JwtUtil jwtUtil;
    private final CookieAuthorizationRequestRepository authorizationRequestRepository;

    /**주석들은 redirect 구현 시 사용예정**/
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        clearAuthenticationAttributes(request, response);
        writeTokenResponse(response, authentication);
    }

    private void writeTokenResponse(HttpServletResponse response, Authentication authentication) throws IOException {
        //JWT 생성
        UserProfile user = (UserProfile) authentication.getPrincipal();
        String userName = user.getName();

        UserRole role = UserRole.ROLE_USER; //일단 다 user로 설정
        String accessToken = jwtUtil.generateToken(userName, role);
        jwtUtil.generateRefreshToken(authentication, response);

        ObjectMapper objectMapper = new ObjectMapper();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        objectMapper.writeValue(response.getWriter(),
                new UserLoginResponse(accessToken));
    }

    private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
        super.clearAuthenticationAttributes(request);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
    }
}

11. User entity implements UserDetails

  • Authentication을 만드는 과정에서 UserDetails 인터페이스를 이용하면 코드가 훨씬 줄어드는 것을 알았다. 이를 적용하며 코드 리팩토링이 필요했지만, 추후 더 유용할 것 같아서 적용하였다.
  • 이를 적용하며 테스트 코드에서 사용하던 @WithMockCustomUser가 필요없어지게 되었다.
  • 또한 OAuth를 적용하며 유저 정보를 저장할 수 있는 field들도 늘었다. 아직은 섞어서 쓰지만 후에는 구분을 하거나 분리가 필요할 것이다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User extends BaseEntity implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String password;
    @Column(unique = true, nullable = false)
    private String userName;
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role;
    /**OAuth2 적용**/
    private String oauthId;
    private String name;
    private String email;
    private String introduction;
    private String refreshToken;
    public User update(String userName, String email){
        this.userName = userName;
        this.email = email;
        return this;
    }
    public static String getUserNameFromAuthentication(Authentication authentication){
        UserDetails user = (UserDetails) authentication.getPrincipal();
        return user.getUsername();
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(this.getRole().name()));
    }
    @Override
    public String getUsername() {
        return this.userName;
    }
    @Override
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)

    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)

    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)

    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    public boolean isEnabled() {
        return true;
    }
}

12. UserDetailsServiceImpl

  • JwtUtil에서 username을 이용하여 UserDetails를 가져오기 위해 사용된다.
  • 이전에는 JwtUtil에서 UserService를 바로 DI 받았다. 그리고 UserService에서도 로그인 시 JwtUtil을 사용하여 순환 참조로 로그인하는 서비스를 따로 떼어냈었다. 하지만 아래 클래스를 JwtUtil에서 주입 받으면 이러한 문제가 해결된다.
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUserName(username)
                .orElseThrow(() -> new UserException(ErrorCode.USERNAME_NOT_FOUND));
    }
}

참고:

Using OAuth 2.0 for Web Server Applications | Authorization | Google Developers

네이버 로그인 개발가이드 - LOGIN

Kakao Developers

https://zkdlu.tistory.com/12

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

https://velog.io/@max9106/OAuth

https://kingofbackend.tistory.com/227

https://alkhwa-113.tistory.com/41

https://cme10575.tistory.com/168

https://ozofweird.tistory.com/586

https://velog.io/@tmdgh0221/Spring-Security-와-OAuth-2.0-와-JWT-의-콜라보

https://europani.github.io/spring/2022/01/15/036-oauth2-jwt.html

🚫 JSON → Map 으로 파싱하기

https://zzznara2.tistory.com/687

→ 필요 없어졌음

OAuth2User에서 name을 가져오기 위해 쓸려고 했으나 Attributes를 이용해서 해결되었다.

OAuth2User의 name은 String이고, attributes는 Map이라 훨씬 가져오기가 편리하였다.

profile
훈이야 화이팅
post-custom-banner

0개의 댓글