이전 글에서 Spring Security 개념에 대해서 정리했다면 이번 글은 Spring Security + JWT 를 적용해보고자 합니다. 앞서 말했다시피 학습과 병행하며 진행된 만큼 아직 개선한 여지가 있는 부분이 있습니다. 글을 읽으시다가 잘못 작성되거나 개선할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다.🙌🏻

 

 
 

목차

1. 구현해보기

  1.1 의존성 추가

  1.2 Spring Security Config

  1.3 JwtAuthorizationFilter

  1.4 CustomAuthenticationProvider

  1.5 CustomUserDetailsService

  1.6 CustomAuthSuccessHandler

  1.7 CustomAuthFailureHandler

 

2. 테스트

 

 구현해보기

 

  • Spring Boot 3.4.0
  • JDK 17
  • Build : Gradle

 

의존성 추가

dependencies {

	implementation 'org.springframework.boot:spring-boot-starter-security'
	testImplementation 'org.springframework.security:spring-security-test'

	//jwt
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
	implementation 'com.sun.xml.bind:jaxb-impl:4.0.1'
	implementation 'com.sun.xml.bind:jaxb-core:4.0.1'
	implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'

}

 

 

Spring Security  Config

 

 1. SecurityFilterChain

Spring Security에서 제공하는 인증, 인가를 위한 필터들의 모임으로 사용하는 목적에 맞게 커스텀하여 필터 체인으로 포함시켜 사용할 수 있다.

 

1.1 SecurityContextPersistenceFilter

   @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
                .csrf(csrf -> csrf.disable()) // JWT 토큰을 사용으로 비활성화
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
                .addFilterBefore(jwtAuthorizationFilter(), BasicAuthenticationFilter.class) //JWT Filter

                //JWT 이용하여 인증
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                // form 기반의 로그인을 비활성화
                .formLogin(login -> login.disable())

                // cumstom으로 구성한 필터 사용
                .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class )

                .httpBasic(withDefaults());

        return http.build();
    }

 

 

 

JwtAuthorizationFilter

 

API 요청이 들어왔을 때, JWT토큰 검증을 위해 Spring Security 필터 체인에 Custom Filter를 추가했습니다.

JwtAuthorizationFilter 는 토큰이 필요하지 않는 API를 제외하고는 모두 수행되며, Authorization 헤더를 통해 JWT의 유효성 검사를 진행합니다. 

@Component
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {


    private String Bearer = "Bearer ";
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 토큰이 필요하지 않는 API의 경우 다음 필터로 이동
        if(request.getRequestURI().startsWith("/api/auth")) {
            response.addHeader("X-Custom-Filter", "Public Access");
            filterChain.doFilter(request, response);

            return;
        }

        try {

            // Client에서 API를 요청할 때 Header 확인
            String token = request.getHeader(HttpHeaders.AUTHORIZATION);

            if(token == null || "".equals(token)) {
                log.error("Token is null");
                setErrorResponse(response, ErrorCode.NOT_EXISTS_TOKEN,null);
            }

            token = token.substring(Bearer.length());

            if(!jwtTokenUtil.isTokenValid(token)) {
                setErrorResponse(response, ErrorCode.INVALID_TOKEN, null);
            }

            filterChain.doFilter(request,response);

        } catch (Exception e) {
            setErrorResponse(response,null, e);
        }

    }

    public static void setErrorResponse(HttpServletResponse response, ErrorCode errorCode, Exception e) throws IOException {

        response.setContentType("application/json;charset=UTF-8");

        if(!"null".equals(e)) {

            if(e instanceof ExpiredJwtException) {
                errorCode = ErrorCode.EXPIRED_TOKEN;
            } else if (e instanceof SignatureException | e instanceof JwtException) {
                errorCode = ErrorCode.INVALID_TOKEN;
            } else if (e instanceof NestedServletException || e instanceof Exception) {
                errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
            }
        }

        PrintWriter writer = response.getWriter();

        response.setStatus(errorCode.getStatus().value());

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(errorCode.getStatus().value()));
        jsonMap.put("message", errorCode.getMessage());

        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

 

CustomAuthenticationFilter

 

로그인 API인 /api/auth/login 으로 요청이 들어오면 해당 필터에 들어오게 되는데, 인증 요청을 처리하는 핵심 인터페이스로 실제 인증 로직을 실행하는 부분입니다.

 

@Slf4j
@Component
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    // login 요청을 하면 실행되는 함수
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        log.info("1. CustomAuthenticationFilter");
        UsernamePasswordAuthenticationToken authenticationToken = null;
        try {

            // request 파라미터로 임시 토큰 발급
            ObjectMapper objectMapper = new ObjectMapper();
            User user = objectMapper.readValue(request.getInputStream(), User.class);

            authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserId(), user.getPassword());

            log.info("Principal : {}", authenticationToken.getPrincipal());
            log.info("Credentials : {}", authenticationToken.getCredentials());

            setDetails(request,authenticationToken);


        } catch (Exception e) {
            log.error(e.getMessage());
        }

        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

}

 

 

 

CustomAuthenticationProvider

 

전달받은 사용자의 아이디와 비밀번호를 기반으로 비즈니스 로직을 처리하여 사용자의 '인증'에 대해서 검증을 수행하는 클래스 입니다.

CustomAuthenticationFilter로부터 생성한 토큰을 통해 'CustomUserDetailsService'를 통해 데이터베이스 내에서 사용자 정보를 조회합니다.

 

@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Resource
    private CustomUserDetailsService customUserDetailsService;
    @NonNull
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        log.info("2. CustomAuthenticationProvider");

        String userId = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        //사용자 인증에 대해서 검증 수행

        UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);

        // DB정보랑 일치한 지 확인
        if(!passwordEncoder.matches(password, userDetails.getPassword())){
            throw new BadCredentialsException("User Info Mismatch");
        }

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

 

CustomUserDetailsService

 

사용자 정보를 데이터베이스에서 가져오는 부분입니다.

@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {

        log.info("UserDetailsService {}" , userId);

        User user = userRepository.findByUserId(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + userId));
        
        return new PrincipalDetails(user);
    }
}

 

CustomAuthSuccessHandler

 

사용자의 '인증'에 대해 성공했을 경우 수행되는 Handler로 성공에 대한 사용자에게 반환값을 구성하여 전달하는 클래스입니다.

@Slf4j
@Configuration
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private JwtProperties jwtProperties;

    @Autowired
    private UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("3.1 CustomAuthSuccessHandler");

        String userId = (String) authentication.getPrincipal();

        // JWT토큰 발급
        String accessToken = jwtTokenUtil.generateAccessToken(userId);
        String refreshToken = jwtTokenUtil.generateRefreshToken(userId);

        // 사용자 refreshToken DB에 저장
        userService.updateUserRefreshToken(userId, refreshToken);

        // 응답규격 생성
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.addHeader(jwtProperties.getHeaderString(), jwtProperties.getTokenPrefix()+accessToken);

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(HttpServletResponse.SC_OK));
        jsonMap.put("message", "정상 처리되었습니다.");

        PrintWriter writer = response.getWriter();
        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

CustomAuthFailureHandler

 

사용자의 '인증'에 대해 실패했을 경우 수행되는 Handler로 실패에 대한 사용자에게 반환값을 구성하여 전달하는 클래스입니다.

@Slf4j
@Configuration
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("3.2 CustomAuthFailureHandler");

        response.setContentType("application/json;charset=UTF-8");

        ErrorCode errorCode = null;

        if(exception instanceof UsernameNotFoundException || exception instanceof BadCredentialsException){
            errorCode = ErrorCode.RESOURCE_NOT_FOUND;
        }  else if (exception instanceof InternalAuthenticationServiceException || exception instanceof AuthenticationCredentialsNotFoundException) {
            errorCode = ErrorCode.UNAUTHORIZED;
        } else {
            errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
        }

        PrintWriter writer = response.getWriter();

        response.setStatus(errorCode.getStatus().value());

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(errorCode.getStatus().value()));
        jsonMap.put("message", errorCode.getMessage());

        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

 

 테스트

 

1. 회원가입

회원가입 API
DB에 해당 사용자정보 저장

 

2. 로그인 

 

case1) 일치한 사용자 정보 없음

 

 

case2) 정상로그인 - Headers에 AccessToken 값 확인가능

'정리 > Spring' 카테고리의 다른 글

Spring Security 란?  (0) 2024.12.10
[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09

 

해당 글은 올해 팀 내 PoC로 진행했던 E2E 테스트 자동화 솔루션 개발 프로젝트를 진행했던 내용을 기록하고자 합니다.

프로젝트 초기 구축을 맡아 진행하면서 인증과 권한 권리를 위해 Spring Security와 JWT를 도입했으며, 회원가입과 로그인 같은 기본적인 인증 절차를 구축하며 프레임워크를 학습하고 적용하는 과정을 통해 많은 것을 배울 수 있었습니다.

다만, 이번 프로젝트는 학습과 병행하며 진행된 만큼 아직 개선한 여지가 있는 부분이 있습니다. 글을 읽으시다가 잘못 작성되거나 개선할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다.🙌🏻

 

 

 
 

목차

1. Spring Security 란?

2. Spring Security 사용하면 뭐가 좋을까?

3. Spring Security 동작 구조를 알아보자.

4. 더 자세히 알아보자.

 

 

 

 Spring Security 란?

Java 애플리케이션 인증(Authentication)과 권한 부여(Authorization)를 모두 제공하는 데 중점을 둔 보안 프레임워크로, Spring Framework를 기반으로 한 애플리케이션에 사용되며, 다양한 보안 관련 요구 사항을 쉽게 해결할 수 있도록 설계되었습니다.

 

주요 특징으로는 

1. 인증(Authentication)

  • 사용자가 누구인지 확인하는 과정으로, 예를 들어 사용자가 로그인 폼에서 입력한 아이디와 비밀번호를 통해 본인 여부를 확인합니다.

2. 권한 부여(Authorization)

  • 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. 부여받은 권한에 따라 접근할 수 있는 페이지가 다를 수 있는데 예를 들어, 관리자는 모든 페이지에 접근할 수 있지만 일반 사용자는 특정 페이지에만 접근할 수 있도록 설정할 수 있습니다.

3. 유연성

  • 데이터베이스 기반 인증, OAuth2, SMAL, LDAP 등의 다양한 인증 방식을 지원하며, 사용자 정의 인증 및 권한 부여 로직을 쉽게 구현할 수 있습니다.

4. 보안 기능 내장

  • 세션 고정, CSRF(Cross-Site Request Forgery) 등과 같은 공격으로부터 보호받을 수 있습니다.

Spring Security 사용하면 뭐가 좋을까?

애플리케이션 보안 기능을 효과적으로 추가하고 관리할 수 있습니다.

 

1. 로그인과 인증 기능을 간편하게 구현

예를 들어, 한 명의 사용자가 쇼핑몰 웹 사이트에 아이디와 비밀번호로 로그인한다고 가정해보면

Spring Security는 로그인 요청을 처리하고 사용자 정보를 데이터베이스에서 확인한 뒤, 인증이 성공하면 자동으로 세션을 생성하고 인증상태를 유지하게 됩니다. 이 과정에서 별도의 인증 로직을 처음부터 구현할 필요 없이, Spring Security에서 제공하는 기능을 활용할 수 있습니다.

 

즉, 인증 로직에 대한 개발 시간 절약과 표준화된 방식으로 높은 안정성을 제공할 수 있습니다.

 

2. URL 별 접근 권한 제어

권한 별 접근할 수 있는 페이지를 세부적으로 설정할 수 있습니다.

예를 들어 쇼핑몰 관리자 페이지 (/admin)은 관리자 권한을 가진 사용자만 접근할 수 있어야하며, 그 외 일반 사용자는 상품 정보, 자신의 주문 내역 등 볼 수 있는 페이지를 제한해야 합니다.

 

Spring Security를 사용하면 아래와 같이 간단한 설정으로 권한 관리를 할 수 있습니다.

http
    .authorizeRequests()
        .antMatchers("/admin").hasRole("ADMIN")  // 관리자만 접근 가능
        .antMatchers("/user/**").hasRole("USER")  // 일반 사용자 접근 가능
        .anyRequest().authenticated();  // 나머지 요청은 인증만 되면 접근 가능

 

3. 다양한 인증 방식 지원

OAuth 2.0 및 JWT와 같은 인증 방식을 통합하여 사용자 인증을 쉽게 처리할 수 있습니다.

REST API를 개발하면서 JWT을 사용해 인증 토큰을 발급하고, 이를 통해 클라이언트와 서버 간 인증을 처리할 수 있습니다.

 

이처럼 Spring Security는 개발자에게 반복적인 보완 관련 작업을 간소화하고, 표준화된 방식으로 높은 보안 수준을 제공하기 때문에 개발효율을 높일 수 있습니다.

 


Spring Security 동작 구조에  대해서 알아보자.

Spring Security는 요청(Request)이 애플리케이션에 도달하기 전에 보안 필터(Security Filters)를 통해 인증과 권한 부여를 처리하는 구조로 설계되어 있습니다. 동작 구조를 주요 단계별로 보면

 

1단계. 요청(Request)이 Security Filter Chain에 진입

Spring Security는 HTTP 요청이 들어오면 이를 처리하기 위해 FilterChainProxy라는 메인 필터를 통해 요청을 가로챕니다.

필터 체인에서 인증 및 권한 부여를 수행하고, 검증이 완료된 이후에 다음 필터 또는 컨트롤러로 전달됩니다.

 

2단계. 인증(Authentication) 처리

사용자가 애플리케이션에 접근하려는 경우, Spring Security는 사용자 인증하는 과정을 거칩니다.

 

2.1 로그인 요청 처리

사용자가 /login과 같은 엔트포인트로 로그인 요청을 보낼 경우, UsernamePasswordAuthenticationFilter가 이를 처리합니다. 이 필터는 아이디와 비밀번호를 읽고, AuthenticationManager에게 인증을 위임합니다.

 

2.2 AuthenticationManager 동작

사용자가 입력한 정보를 기반으로 실제 인증을 수행합니다. 일반적으로 UserDetailsService를 호출하여 사용자 정보를 데이터베이스에서 서 가져와 저장된 정보와 비교하여 인증 성공 여부를 결정합니다.

 

2.3 인증 성공/실패

인증에 성공할 경우, 사용자의 인증 정보를 SecurityContext에 저장하고 다음 필터로 요청을 전달합니다.

인증에 실패할 경우, 에러 응답(401 Unauthorized 등)을 반환합니다.

 

3. 권한 부여 (Authorization) 처리

인증이 성공한 후, 사용자가 요청한 리소스에 접근할 권한이 있는지 확인하는 단계로

 

3.1 SecurityContext에서 인증 정보 확인

SecurityContext에 저장된 인증 정보를 확인하여 사용자의 역할(Role) 또는 권한(Authority)를 가져옵니다.

 

3.2 AccessDecisionManager 동작

사용자가 요청한 URL, 메서드(GET, POST등), 리소스 등에 대해 접근 가능한지 확인합니다. 권한이 없으면 403 Forbidden 에러를 반환합니다.

 

4. FilterChain 완료 및 컨트롤러 호출

모든 필터를 성공적으로 통과한 요청은 최종적으로 애플리케이션에 컨트롤러로 전달되며, 컨트롤러는 요청을 처리하고 응답을 반환합니다.

 

5. 응답 (Response) 반환 및 세션/토큰 관리

응답을 반환할 때, Spring Security는 인증 정보를 관리하거나 상태를 업데이트 합니다.

 

세션 기반 인증의 경우, 인증 정보를 세션에 저장하고 클라이언트는 세션 쿠키를 통해 인증 상태를 유지합니다.

JWT 기반 인증의 경우, 인증 성고 시 JWT 토큰을 생성하여 클라이언트에 반환합니다. 클라이언트는 이후 요청마다 이 토큰을 헤더에 포함시켜 인증을 유지합니다.

 

 

더 자세히 알아보자.

 

먼저 사용자 인증 요청이 오면

 

1. DelegatingFilterProxy 

Servlet Container 와 Spring loC Container를 연결해주는 필터로 springSecurityFilterChain 이름으로 생성된 Bean을 찾아 요청을 위임하는 역할을 한다.

 

2. FilterChainProxy

springSecurityFilterChain의 이름으로 생성되는 필터 빈으로 DelegatingFilterProxy로부터 요청을 위임 받고 실제로 보안을 처리하는 역할을 한다. (Spring Security 초기화 시 생성되는 필터들을 관리하고 제어)

 

2-1 SecurityContextPersistenceFilter

SecurityContextRepository에서 SecurityContext를 가져오거나 생성하는 역할

 

SecurityContext는 인증 객체(Authentication)가 저장되는 저장소이며 setAuthentication() 메소드를 통해 Authentication을 설정할 수 있다.

 

SecurityContextPersistenceFilter 를 거치면 SecurityContextRepository에서 SecurityContext를 가져오는데

 

1) 처음 인증하는 사용자일 경우

SecurityContext를 생성하고 SecurityContextHolder안에 저장하고 다음 필터 실행

 

2) 인증 이력이 있는 사용자일 경우

이미 존재하는 SecurityContext를 가져와 SecurityContextHolder 에 저장하고 다음 필터 실행

 

 

2-2 LogoutFilter

로그아웃에 대한 처리 담당. 세션 무효화, 인증 토큰 삭제, SecurityContext에서 토큰 삭제 등 로그아웃 시 필요한 작업 수행

 

 

2-3 UsernamePasswordAuthenticationFilter

Form Based Authentication 방식으로 인증을 처리할 때 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터

해당 인증 방식외에 다른 로그인 방식을 사용한다면 disable() 을 통해 폼 기반 로그인 방식 비활성화 할 수 있다.

 

HttpServletRequest 객체에서 아이디, 패스워드 정보를 가져와 Authentication  객체에 저장하고 AuthenticationManager에게 인증처리를 전달한다.

 

2-4 ConCurrentSessionFilter

현재 사용자 계정으로 인증을 받은 사용자가 두 명 이상힐 때 실행되는 필터로 매 요청마다 사용자의 세션 만료 여부를 체크하고 세션이 만료되었을 경우 즉시 만료처리 하는 역할

 

2-5 RememberMeAuthenticationFilter

세션이 만료되거나 무효화되어서 세션안에 있는 SecurityContext 내 인증 객체가 null일 경우 실행되는 필터로 사용자가 요청하는 Reqeust Header에 remeber-me cookie 값을 헤더에 저장한 상태로 요청이 왔을 때 접속한 사용자 대신 인증처리를 시도

 

2-6 AnonymousAuthenticationFilter

인증 시도를 하지 않고 권한도 없이 어떤 자원에 바로 접속을 시도하는 경우 실행되는 필터로 annonymouseAuthenticationToken을 만들어 SecurityContext 객체에 저장하는 역할

 

2-7 SessionManagementFilter

현재 세션에 SecurityContext가 없거나 세션이 null인 경우에 동작되는 필터로 아래 3가지 작업 수행

 

Register SessionInfo : 사용자 세션 정보 등록

인증에 성공한 SecurityContext를 세션에 저장

 

SessionFixation : 인증을 시도하기 전 이전 쿠키가 삭제되고 성공한 시점에 새로운 쿠키 발급

 

ConcurrentSession : 사용자 계정으로 동일한 세션이 존재하는지 확인

 

2-8 ExceptionTranslationFilter

Filter Chain을 거치면서  발생하는 인증, 인가 예외가 발생할 때 실행되는 필터

 

AuthenticationException : 인증 예외 처리

 

AccessDeniedException : 인가 예외 처리

 

2-9 FilterSecurityInterceptor

권한 부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 처리

 

'정리 > Spring' 카테고리의 다른 글

Spring Security _ 2탄 ( 이제 JWT를 곁들인.. )  (0) 2024.12.11
[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09

Dependency 의존관계란?


'A가 B를 의존한다.' 라는 표현은 어떤 의미일까? 토비의 스프링에서는 다음과 같이 정의한다.

 

의존대상 B가 변하면, 그것이 A에 영향을 미친다.

 

즉, B의 기능이 수정된다면 A에게까지 영향을 미친다.

 

다음 예시를 보며 이해해보자.

"햄버거 가게 요리사는 햄버거 레시피에 의존한다." 는 말은 햄버거 레시피가 변경되었을 때, 변경된 레시피에 따라서 요리사는 햄버거 만드는 방법을 바꿔야한다. 레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에 "요리사는 레시피에 의존한다"고 말할 수 있다.

 

코드로 표현해보면 다음과 같다.

class BurgerChef {
    private HamBurgerRecipe hamBurgerRecipe;

    public BurgerChef() {
        hamBurgerRecipe = new HamBurgerRecipe();        
    }
}

여기에서 HamBurgerRecipe만을 의존할 수 있는 구조로 되어있다. 만약 더 다양한 햄버거 레시피를 의존 받을 수 있게 구현하려면 인터페이스로 추상화해야한다.

 

이를 코드로 표현해보면 다음과 같다.

class BurgerChef {
    private BurgerRecipe burgerRecipe;

    public BurgerChef() {
        burgerRecipe = new HamBurgerRecipe();
        //burgerRecipe = new CheeseBurgerRecipe();
        //burgerRecipe = new ChickenBurgerRecipe();
    }
}

interface BugerRecipe {
    newBurger();
    // 이외의 다양한 메소드
} 

class HamBurgerRecipe implements BurgerRecipe {
    public Burger newBurger() {
        return new HamBerger();
    }
    // ...
}

class CheeseBurgerRecipe implements BurgerRecipe {
	public Buerger newBurger() {
    	return new CheeseBurger();
    }
    // ...
}

의존관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수 있고 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.

 

그럼 DI(Dependency Injection)은 뭔가?


위에 코드를 보면 BurgerChef 내부적으로 의존관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다. 만약 어떤 BurgerRecipe를 만들지를 버거 가게 사장님이 정하는 상황을 가정해보자. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부(사장님)에서 결정하고 주입하는 것이다.

 

이처럼 의존관계를 외부에서 결정하고 주입하는 것이 DI(의존관계 주입)이다.

DI를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.

 

DI 구현 방법


DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입해 DI 구현이 완성된다.

 

"Burger 레스토랑 주인이 어떤 레시피를 주입하는지 결정하는 예시로 설명하고자 한다."

 

[생성자 이용]

class BurgerChef {
    private BurgerRecipe burgerRecipe;

    public BurgerChef(BurgerRecipe burgerRecipe) {
        this.burgerRecipe = burgerRecipe;
    }
}

class BurgerRestaurantOwner {
    private BurgerChef burgerChef = new BurgerChef(new HamburgerRecipe());

    public void changeMenu() {
        burgerChef = new BurgerChef(new CheeseBurgerRecipe());
    }
}

 

[메소드 이용]

class BurgerChef {
    private BurgerRecipe burgerRecipe = new HamburgerRecipe();

    public void setBurgerRecipe(BurgerRecipe burgerRecipe) {
        this.burgerRecipe = burgerRecipe;
    }
}

class BurgerRestaurantOwner {
    private BurgerChef burgerChef = new BurgerChef();

    public void changeMenu() {
        burgerChef.setBurgerRecipe(new CheeseBurgerRecipe());
    }
}

 

 

DI의 장점은 뭘까?


1. 의존성이 줄어든다.

의존성이 높을 수록 대상이 변화했을 때, 이에 맞게 다른 것도 수정해야한다. DI로 구현했을 경우, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게 된다.

 

2. 재사용성이 높은 코드가 된다.

기존에 BurgerChef 내부에서만 사용되었던 BurgerRecipe를 별도로 구분하여 구현한다면, 다른 클래스에서 재사용할 수 있다.

 

3. 가독성이 높아진다.

기능들을 별도로 분리하게 되어 자연스럽게 가독성이 높아진다.

 

 

관점 지향 프로그래밍 (AOP)란 뭘까?


AOP는 OOP를 돕는 보조적인 기술로, 관심사의 분리의 문제를 해결하기 위해 만들어진 프로그래밍 패러다임이다.

여러 객체에 공통으로 적용할 수 있는 기능을 분리함으로써 재사용성을 높일 수 있다.

 

만약 메서드를 실행할 때마다 공통적으로 확인하고 싶은 정보가 있는데, 모든 메서드에 해당 로그 코드를 작성하면 코드가 길어질 뿐만아니라 가독성이 떨어질 것이다.

 

이를 AOP로 분리시킨다면..

 

@Aspect
@Component
public class ParameterAop {

    //com/example/aop/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut() {}

    //cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
    @Before("cut()")
    public void before(JoinPoint joinPoint) {
		
        //실행되는 함수 이름을 가져오고 출력
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName() + "메서드 실행");

        //메서드에 들어가는 매개변수 배열을 읽어옴
        Object[] args = joinPoint.getArgs();
		
        //매개변수 배열의 종류와 값을 출력
        for(Object obj : args) {
            System.out.println("type : "+obj.getClass().getSimpleName());
            System.out.println("value : "+obj);
        }
    }

    //cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
    //@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
    @AfterReturning(value = "cut()", returning = "obj")
    public void afterReturn(JoinPoint joinPoint, Object obj) {
        System.out.println("return obj");
        System.out.println(obj);
    }
}

AOP 클래스를 구현함으로써 이전보다 코드가 훨씬 간결해지고 가독성이 높아지는 것을 확인할 수 있다.

 

'정리 > Spring' 카테고리의 다른 글

Spring Security _ 2탄 ( 이제 JWT를 곁들인.. )  (0) 2024.12.11
Spring Security 란?  (0) 2024.12.10
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09

API란?

  • Application Programming Interface
  • 응용 프로그램에서 사용할 수 있도록, 운영체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스

👉🏻 OPEN API는 뭘까?

  • 프로그래밍에서 사용할 수 있는 개방되어 있느 상태의 인터페이스
  • NAVER, KAKAO 등 포털 서비스 사이트 등 다양한 데이터를 외부 응용 프로그램에서 사용할 수 있도록 OPEN API를 제공

REST란?

  • Representational State Transfer의 약어로 하나의 URL은 하나의 고유한 리소스를 대표하도록 설계된다는 개념에 전송방식을 결합해서 원하는 작업 지정

  • HTTP URI를 통해 제어할 자원을 명시하고 HTTP Method를 통해 해당 자원을 제어하는 명령을 내리는 방식의 아키텍처

  • HTTP Method
    • POST : 리소스 생성
    • GET : 리소스 조회
    • PUT : 리소스 수정
    • DELETE : 리소스 삭제

👉🏻GET / POST 차이

  • GET방식 : 클라이언트에서 서버로 어떤 리소스로부터 필요한 정보를 요청하기 위해 사용하는 메서드 EX) 게시판 게시물 조회
    • 데이터 URL주소 끝에 파라미터로 쿼리 스트링을 포함하여 전송
    • 요청 길이에 제한있다.
    • 파라미터에 붙여서 전달하기 때문에 보안에 취약하다.
    • 멱등이다.

  • POST방식 : 클라이언트에서 서버로 리소스를 생성하거나 업데이트하기 위해 데이터를 보낼 때 사용하는 메서드 EX) 게시판 글 작성
    • 데이터 HTTP 메시지 BODY부분에 담아서 전송
    • 요청 길이에 제한이 없다.
    • BODY에 담아서 보내기 때문에 데이터 노출되지 않아 보안에 강함
    • 멱등이 아니다.


    멱등이 뭐야❓
    • 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질
    • 즉, GET은 리소스를 조회만하기 때문에 여러 번 요청하더라도 응답은 똑같을 것이다. 하지만 POST는 리소스를 새로 생성하거나 업데이트할 때 사용되기 때문에 멱등이 아니라고 볼 수 있다.

👉🏻Swagger란?

  • 간단한 설정으로 프로젝트의 API 목록을 웹에서 확인 및 테스트 할 수 있게 해주는 라이브러리

'정리 > Spring' 카테고리의 다른 글

Spring Security 란?  (0) 2024.12.10
[Spring] DI & AOP  (0) 2021.09.24
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09
[Spring Boot + Vue.js] 구글로그인  (0) 2021.08.18

Spring MVC란?

👉🏻 model, view, controller의 합성어로 소프트웨어 디자인 패턴

  • Model : Controller에서 받은 데이터를 저장하는 역할
  • View : jsp 파일등과 같이 화면에 시각적으로 표현해주는 역할
  • Controller : 사용자의 요청을 받고 응답을 하는 로직을 담당. 접근한 url에 따라 요청사항을 파악하고 그에 맞는 데이터를 model에 의뢰하고 view에 반영해서 사용자에게 보여주는 역할

👉🏻Spring MVC 요청흐름

  • Request → DispatcherServlet (web.xml) → Controller → Logic처리(service, db접근) → View 전달 → response

  • DispatcherServlet : 모든 request를 받는 관문. request를 실제로 처리할 Controller에게 클라이언트의 요청을 전달하고, controller가 리턴한 결과값을 View에게 전달하여 알맞은 응답 생성
  • Controller : 클라이언트의 요청을 처리한 뒤, Model을 호출하고 그 결과를 DispatcherServlet에게 알려줌
  • View : Controller의 처리결과를 보여울 응답화면 생성

👉🏻VO, DTO, DAO란?

  • VO(Value Object) : 실제 데이터만 저장하는 클래스
  • DTO(Data Transfer Object) : 데이터를 주고 받기 위해 사용하는 클래스
  • DAO(Data Access Object) : DB에 접근하여 실제 데이터를 조회/조작하는 클래스 Repository 또는 Mapper에 해당

👉🏻MVC Pattern 특징

  • 어플리케이션의 확장을 위해 model, view, controller 세 영역으로 분리
  • 컴포넌트의 변경이 다른 영역의 컴포턴트에 영향을 미치지 않는다. = 유지보수 용이하다.
  • 컴포넌트 간의 결합성이 낮아 프로그램 수정이 용이하다. = 확장성이 뛰어나다.

장점

  • 화면과 비즈니스 로직을 분리해서 작업 가능
  • 영역별 개발로 확장성 뛰어남
  • 표준화된 코드를 사용하므로 공동작업 용이 및 유지보수성 좋음

단점

  • xml를 기반으로하는 프로젝트 설정은 많은 시간 소요
  • Tomcat과 같은 WAS 별도 설치

해결책( = Spring Boot)

  • 자동설정을 도임하여 DispatcherServlet과 같은 설정 시간 감소
  • spring-boot-starter로 외부 도구 사용
  • 내장 톰캣 제공하여 별도의 WAS 필요 X


웹 서버 VS WAS❓

  • 웹 서버(Apache) : 클라이언트가 웹 브라우저의 어떠한 페이지 요청을 하면 웹 서버에서 그 요청을 받아 정적 컨텐츠(HTML, CSS 등 즉시 응답가능한 컨텐츠)를 제공하는 서버
  • WAS(Tomcat) : 웹 서버 단독으로 처리할 수 없는 데이터베이스의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공하는 서버

👉🏻그럼 웹 서버는 동적 컨텐츠를 제공하지 못하나?

  • 웹 서버가 동적 컨텐츠 요청을 받으면 WAS에게 해당 요청을 넘겨주고, WAS에어 처리한 결과를 클라이언트에게 전달해주기도 한다!

👉🏻 그럼 WAS를 쓰면 되는거 아닌가?

  • WAS는 DB조회 및 다양한 로직처리하는 데 집중하기 때문에 단순한 정적 컨텐츠를 웹 서버에 맡겨 기능을 분리시킴으로써 서버 부하 방지!
  • 만약 WAS가 정적 컨텐츠 요청까지 처리한다면, 동적 컨텐츠 처리가 지연되면서 수행 속도도 느려질뿐더러 이로 인해 페이지 노출 시간이 지연되는 문제가 발생하여 효율성이 크게 떨어진다.

👉🏻 그럼 Apache Tomcat은 다른건가?

  • Tomcat에 정적 컨텐츠를 처리하는 기능이 추가되어, Tomcat가 Apache기능을 포함하고 있어 Apache Tomcat이라고 부른다!

👀참고

https://codechasseur.tistory.com/25 (웹서버 WAS차이)

https://minchoi0912.tistory.com/93 (Spring MVC)

'정리 > Spring' 카테고리의 다른 글

[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09
[Spring Boot + Vue.js] 구글로그인  (0) 2021.08.18
[Spring Boot] 이메일 인증 회원가입하기 구현  (2) 2021.07.23

Spring이란?

  • Java언어 기반의 프레임워크
  • Java로 다양한 어플리케이션을 만들기 위한 프로그래밍 틀

프레임워크❓

  • 프로그램 기본 구조(뼈대)
  • 원하는 기능 구현에만 집중하여 빠르게 개발할 수 있도록 기본적으로 필요한 기능을 갖추고 있는 것.

쉽게 이해할 수 있도록 집을 짓는 것으로 비유를 해보자.

사용자가 집을 직접 짓기 위해 설계도를 직접 그리고 각각의 기초 작업들을 일일이 하는 것 보다, 전문가들의 도움을 받아 작업을 하면 더욱 쉽고 효율적으로 집을 지을 수 있는 것처럼 프레임워크를 사용한다면 프레임워크가 제공하는 여러 기능들을 사용해서 빠르고 효율적으로 프로그램을 구축할 수 있을 것이다.

 

프레임워크를 사용하면 뭐가 좋을까❓


개발 프로세스 간소화

프레임워크에서 제공하는 여러 도구와 패키지는 개발자가 스크립트를 처음부터 작성할 필요가 없고, 이미 만들어진 코드를 재사용하게 되므로 재사용성을 높이고, 시간과 비용을 아낄 수 있다.

 

유지보수 용이

프레임워크를 사용한다면 코드가 체계적이고 상대적으로 정형화되기 때문에, 개발자가 중간에 교체되더라도 위험이 적고, 이후 소스코드의 유지보수도 상당히 용이해진다.

 

보안

개발자는 일반적으로 SQL Injection, CSRF 등 외부 공격을 방어하기 위한 추가적인 소스코드를 작성해야하지만, 프레임워크를 사용하면 이러한 일들을 할 필요가 없다. 대부분의 프레임워크와 함께 제공되는 보안 기능들은 개발자가 웹 사이트 혹은 애플이케이션을 보호할 수 있는 방법을 제공하기 때문이다.

 

그럼 프레임워크의 단점은 없을까❓


사전학습

다양한 기능을 제공하고, 미리 만들어져있는 기능을 사용하기 위해서는 학습이 필요하다. 새로운 프레임워크를 학습하고 사용하기 위해서는 많은 공부가 필요하다.

 

제약사항

제공하고 있는 기능들 외에, 원하는 옵션을 추가하는 것에 굉장히 보수적일 수 있다.

사용법이 정해져 있고, 기본적으로 설계된 구조를 바탕으로 코드를 작성하고 기능을 완성해야하기 때문에 코드를 유연하게 개발하는데 있어서 한계가 있을 수 있다.

 

크기

프레임워크는 많은 기능을 제공한다. 이는 개발하면서 필요하지 않은 기능도 포함된다는 것을 의미하고, 불필요한 기능이 메모리를 차지하기 때문에 리소스 낭비로 이어질 수 있다.

 

 

💡스프링(Spring)의 특징 -⭐ 결합도를 낮춰 유연할 개발 가능하게 해준다!


  • 제어의 역전(IOC : Inversion of Control)
    • 프로그램의 생명주기에 대한 제어권이 웹 애플리케이션 컨테이너에 있다. 
    • 사용자가 직접 new 연산자를 통해 인스턴스를 생성하고 메서드를 호출하는 일련의 생명주기에 대한 작업들을 스프링에 위임
    • 객체관리 주체가 프레임워크(Container)가 되기 때문에 개발자는 로직에 집중할 수 있다는 장점이 있다.

 

  • 의존성 주입(DI)
    • 객체 사이에 필요한 의존 관계에 대해서 스프링 컨테이너가 자동으로 연결해 주는 것
    • DI를 이용하여 빈(Bean) 객체 관리, 스프링 컨테이너에 클래스를 등록하여 스프링이 클래스의 인스턴스를 관리해 준다.
    👉🏻스프링 컨테이너에 빈(Bean)을 등록하고 설정하는 방법
    1. XML 설정을 통한 DI - applicationContext.xml에서 설정
    2. 어노테이션을 이용한 DI
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //DI가 없는 예제
    @RestController
    public class Controller{
        Service service = new Service(); //객체간의 결합도↑
     
    @RequestMapping("/hello")
    public String hello(){
            return service.helloMessage();
        }
    }
    cs

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //DI가 있는 예제
     
    //service
    @Component
    public class Service{
        public String helloMessage(){
            return "hello~"
        }
    }
     
    //controller
    @RestController
    public class Controller{
        @Autowired
        Service service;
     
        @RequestMappint("/hello")
        public String hello(){
            return service.helloMessage();
        }
     
    }
    cs
    → 어노테이션으로 Service 객체의 인스턴스를 쉽게 얻을 수 있다. = 결합도↓

 

  • 관점 지향 프로그래밍(AOP)
    • 주요 핵심 기능과 핵심 기능 구현을 위한 부가적인 기능(로깅 작업, 데이터베이스 연결 및 트랜잭션 처리, 파일 입출력 등) 구현을 분리하여 각각의 관점별로 묶어서 개발하는 방식

그럼 왜 스프링 부트가 필요할까❓


  • Spring MVC 프레임워크를 사용할 때 applicationContext.xml이외에 다양한 설정을 통해 의존성을 주입해야한다.
  • 기본 프로젝트를 셋팅하는데 많은 시간 소요

그럼 Spring Boot는 뭐가 다를까❓


  • Spring Boot는 따로 설정할 필요 없이 단지 실행만 하면 된다!
  • spring-boot-starter-web을 사용하면 종속된 모든 라이브러리를 알맞게 찾아서 가져오기 때문에 종속성이나 호환 버전에 대해 신경 쓸 필요가 없다!

공부하면서 작성한 글입니다. 틀린 내용이 있다면 알려주세요!🙏🏻

 

 

👀 참고

https://dzone.com/articles/spring-vs-spring-boot

https://msyu1207.tistory.com/entry/Spring-VS-Spring-Boot-차이점을-알아보자

'정리 > Spring' 카테고리의 다른 글

[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring Boot + Vue.js] 구글로그인  (0) 2021.08.18
[Spring Boot] 이메일 인증 회원가입하기 구현  (2) 2021.07.23

구글 로그인 

  • 구글 로그인 버튼 클릭
1
2
3
<a href="https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.email&response_type=code&client_id={client_id}&redirect_uri={redirect_uri}">
<img :src="require('@/assets/images/google-icon.png')"/>
</a>
cs

 

  • 구글 로그인 진행 후, 리다이렉트 페이지로 이동하면 url에 Authorization code 확인!

 

  • Authorization code axios를 통해 백엔드에 전달
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let googleCode = new URL(window.location.href).searchParams.get("code");
    if(googleCode != null){
      console.log("구글로그인 시도");
      store.dispatch("auth/requestGoogleToken", googleCode)
      .then(function(result){
        console.log(result)
        if (result.data.idToken) {
          console.log("구글 result: ", result.data.idToken)
          console.log("구글 email : ",result.data.email )
          localStorage.setItem("jwt", result.data.idToken);
          store.commit("auth/setToken", result.data.idToken);
          store.commit("auth/setEmail", result.data.email);
          store.commit("auth/setProvider","google"); // 로그아웃할때 방식 다 달라서 구분용
        }
        router.push({
          path: "/"
        });
      }).catch(function(error){
        console.log(error)
      })
    }
cs

 

  • Authorization code를 이용해 구글 access_token 및 id_token 발행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.springframework.stereotype.Service;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
 
@Service
public class GoogleService {
    public String getAccessToken (String authorize_code) {
        String access_Token = "";
        String id_Token = "";
        String reqURL = "https://oauth2.googleapis.com/token";
 
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
 
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
            StringBuilder sb = new StringBuilder();
            sb.append("grant_type=authorization_code");
            sb.append("&client_id="); //수정 할것
            sb.append("&redirect_uri="); //수정 할것
            sb.append("&client_secret="); //수정 할것
            sb.append("&code=" + authorize_code);
            bw.write(sb.toString());
            bw.flush();
            int responseCode = conn.getResponseCode();
            System.out.println("responseCode : " + responseCode);
 
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            String result = "";
 
            while ((line = br.readLine()) != null) {
                result += line;
            }
            System.out.println("response body : " + result);
 
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(result);
 
            access_Token = element.getAsJsonObject().get("access_token").getAsString();
            id_Token = element.getAsJsonObject().get("id_token").getAsString();
            System.out.println("access_token : " + access_Token);
            System.out.println(id_Token);
 
            br.close();
 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
        return access_Token;
    }
  
}
cs

 

  • 결과
1
2
3
4
responseCode : 200
response body : {  "access_token": "ya29.a0ARrdaM-w7dZpSKr4865j6fZ4SQcAbyHw9B2jG0Rd7vLpjf45gHlmbJ8YSEg6klSy8ElFFW-JanHQxd2u8zs7aUKTPZdLY9K28mx1k7a4J0JXGjX-k6MYqd0GaiNN9EV5wvXa_gpS7i6M3R36dGOkyvky9Wje",  
"expires_in": 3599,  "scope": "https://www.googleapis.com/auth/userinfo.email openid",  "token_type": "Bearer",  
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZlZjRiZDkwODU5MWY2OTdhOGE5Yjg5M2IwM2U2YTc3ZWIwNGU1MWYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIyMjk1MTExNDAxMTgtMzFkNHZwMTYwYzdkZDFsZDRnMjcxODBmbXExcWVzZzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjk1MTExNDAxMTgtMzFkNHZwMTYwYzdkZDFsZDRnMjcxODBmbXExcWVzZzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE3MjkxNDg1Mzc4Mzg3NTY3NTUiLCJlbWFpbCI6InduZHVzeDFAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJBOE94NFhPa2oyblV0aU1VUmhUeFBnIiwiaWF0IjoxNjI5Mjk1Njk3LCJleHAiOjE2MjkyOTkyOTd9.gsTWNfzCEZYniwwI9a_CoDmn5Kd7SUbyJLYxG9GbdBFpeXiS7yR_Zl3J2kDtqieDUgbt2EMZP4MlDEGbB0kglYWJXbE54kmbVKlmzMKK41GoLvohNnSEQUJHgj0bPFkBqJm8Z3DLhmWQJmqDczinUbHmYSbvcHgflRSJs1_0xAM7-xtmVE55rJoR0JzBkIjKhXGsGue9791lP0M0fM9y6SJxmkLJLv-1c1eoKIgY4cTbIYVOs29TwKRSQGSwTqsVBq63WinYFrt0ECc_e4RB21wcmDZx2aQe_SDEw4iqayWdFEnSYiyyit6XIpkLYXdaa_3F4RWpxL1biuJgMqSBWA"}
cs

 

구글 id_token으로 로그인한 유저 정보 얻기

  • id_token을 이용해 구글 이메일, 프로필 사진 등 유저 정보 얻기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.babble.api.service;
 
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.springframework.stereotype.Service;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
 
@Service
public class GoogleService {
  
    public String getUserInfo (String id_token) {
        String reqURL = "https://oauth2.googleapis.com/tokeninfo?id_token="+id_token;
        String email="";
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setDoOutput(true);
 
            int responseCode = conn.getResponseCode();
            System.out.println("responseCode : " + responseCode);
 
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            String result = "";
 
            while ((line = br.readLine()) != null) {
                result += line;
            }
            System.out.println("response body : " + result);
 
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(result);
            email = element.getAsJsonObject().get("email").getAsString();
 
 
            br.close();
 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return email;
    }
 
}
cs

 

  • 결과 - reqURL 에 따라 이메일, 프로필, 토큰 만료시간 등 여러 유저 정보 확인 가능!
1
2
responseCode : 200
response body : {  "iss": "https://accounts.google.com",  "azp": "229511140118-31d4vp160c7dd1ld4g27180fmq1qesg8.apps.googleusercontent.com",  "aud": "229511140118-31d4vp160c7dd1ld4g27180fmq1qesg8.apps.googleusercontent.com",  "sub": "111729148537838756755",  "email": "wndusx1@gmail.com",  "email_verified": "true",  "at_hash": "A8Ox4XOkj2nUtiMURhTxPg",  "iat": "1629295697",  "exp": "1629299297",  "alg": "RS256",  "kid": "6ef4bd908591f697a8a9b893b03e6a77eb04e51f",  "typ": "JWT"}
cs

'정리 > Spring' 카테고리의 다른 글

[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09
[Spring Boot] 이메일 인증 회원가입하기 구현  (2) 2021.07.23

현재 진행하고 있는 프로젝트에서 사용하고 있는 이메일을 이용해 회원가입을 진행할 수 있도록 구현했습니다.

네이버, 구글, 다음 등의 가입되어있는 이메일을 이용하여 해당 이메일로 인증번호를 보낸 뒤,

인증번호가 일치한 경우 회원가입이 될 수 있도록 했습니다.

 

저는 구글 메일을 이용해 구현했습니다.

 

 
 

구현 순서

1. dependency 추가

1. properties 추가

2. EmailConfig 추가

3. EmailService 추가

4. EmailServiceImpl 추가

5. Controller 추가

 

구현하기 전 간단한 설정이 필요합니다!!

 

https://www.google.com/settings/security/lesssecureapps에서 보안 수준이 낮은 앱의 액세스 활성화

활성화를 해줘야 메일을 받을 수 있어요!

 

dependency 추가

build.gradle

1
2
3
dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-mail'
}
cs

 

 

properties

email.properties

1
2
3
4
5
6
7
8
9
10
11
mail.smtp.auth=true
mail.smtp.starttls.required=true
mail.smtp.starttls.enable=true
mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
mail.smtp.socketFactory.fallback=false
mail.smtp.port=465
mail.smtp.socketFactory.port=465
 
#admin 구글 계정
AdminMail.id = 
AdminMail.password =
cs

 

 

EmailConfig

EmailConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.Properties;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
 
 
@Configuration
@PropertySource("classpath:email.properties")
public class EmailConfig {
 
    @Value("${mail.smtp.port}")
    private int port;
    @Value("${mail.smtp.socketFactory.port}")
    private int socketPort;
    @Value("${mail.smtp.auth}")
    private boolean auth;
    @Value("${mail.smtp.starttls.enable}")
    private boolean starttls;
    @Value("${mail.smtp.starttls.required}")
    private boolean startlls_required;
    @Value("${mail.smtp.socketFactory.fallback}")
    private boolean fallback;
    @Value("${AdminMail.id}")
    private String id;
    @Value("${AdminMail.password}")
    private String password;
 
    @Bean
    public JavaMailSender javaMailService() {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
        javaMailSender.setHost("smtp.gmail.com");
        javaMailSender.setUsername(id);
        javaMailSender.setPassword(password);
        javaMailSender.setPort(port);
        javaMailSender.setJavaMailProperties(getMailProperties());
        javaMailSender.setDefaultEncoding("UTF-8");
        return javaMailSender;
    }
    private Properties getMailProperties()
    {
        Properties pt = new Properties();
        pt.put("mail.smtp.socketFactory.port", socketPort);
        pt.put("mail.smtp.auth", auth);
        pt.put("mail.smtp.starttls.enable", starttls);
        pt.put("mail.smtp.starttls.required", startlls_required);
        pt.put("mail.smtp.socketFactory.fallback",fallback);
        pt.put("mail.smtp.socketFactory.class""javax.net.ssl.SSLSocketFactory");
        return pt;
    }
}
cs

 

 

EmailService

EmailService.java

1
2
3
public interface EmailService {
    String sendSimpleMessage(String to)throws Exception;
}
cs

 

EmailServiceImpl

EmailServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.util.Random;
 
import javax.mail.Message.RecipientType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
 
@Service
public class EmailServiceImpl implements EmailService{
 
    @Autowired
    JavaMailSender emailSender;
 
    public static final String ePw = createKey();
 
    private MimeMessage createMessage(String to)throws Exception{
        System.out.println("보내는 대상 : "+ to);
        System.out.println("인증 번호 : "+ePw);
        MimeMessage  message = emailSender.createMimeMessage();
 
        message.addRecipients(RecipientType.TO, to);//보내는 대상
        message.setSubject("Babble회원가입 이메일 인증");//제목
 
        String msgg="";
        msgg+= "<div style='margin:100px;'>";
        msgg+= "<h1> 안녕하세요 Babble입니다. </h1>";
        msgg+= "<br>";
        msgg+= "<p>아래 코드를 회원가입 창으로 돌아가 입력해주세요<p>";
        msgg+= "<br>";
        msgg+= "<p>감사합니다!<p>";
        msgg+= "<br>";
        msgg+= "<div align='center' style='border:1px solid black; font-family:verdana';>";
        msgg+= "<h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>";
        msgg+= "<div style='font-size:130%'>";
        msgg+= "CODE : <strong>";
        msgg+= ePw+"</strong><div><br/> ";
        msgg+= "</div>";
        message.setText(msgg, "utf-8""html");//내용
        message.setFrom(new InternetAddress("properties email쓰세용!","Babble"));//보내는 사람
 
        return message;
    }
 
    public static String createKey() {
        StringBuffer key = new StringBuffer();
        Random rnd = new Random();
 
        for (int i = 0; i < 8; i++) { // 인증코드 8자리
            int index = rnd.nextInt(3); // 0~2 까지 랜덤
 
            switch (index) {
                case 0:
                    key.append((char) ((int) (rnd.nextInt(26)) + 97));
                    //  a~z  (ex. 1+97=98 => (char)98 = 'b')
                    break;
                case 1:
                    key.append((char) ((int) (rnd.nextInt(26)) + 65));
                    //  A~Z
                    break;
                case 2:
                    key.append((rnd.nextInt(10)));
                    // 0~9
                    break;
            }
        }
 
        return key.toString();
    }
    @Override
    public String sendSimpleMessage(String to)throws Exception {
        // TODO Auto-generated method stub
        MimeMessage message = createMessage(to);
        try{//예외처리
            emailSender.send(message);
        }catch(MailException es){
            es.printStackTrace();
            throw new IllegalArgumentException();
        }
        return ePw;
    }
 
}
cs

★ 저는 인증코드 일치 여부를 비교해주기 위해서 sendSimpleMessage() 메서드를 void가 아닌 String으로 만들어주었습니다!

 

 

Controller 

Controller.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/emailConfirm")
    @ApiOperation(value = "회원 가입시 이메인 인증", notes = "기존사용하고 있는 이메일을 통해 인증")
    @ApiResponses({
            @ApiResponse(code = 200, message = "성공"),
            @ApiResponse(code = 401, message = "인증 실패"),
            @ApiResponse(code = 404, message = "사용자 없음"),
            @ApiResponse(code = 500, message = "서버 오류")
    })
    public ResponseEntity<extends BaseResponseBody> emailConfirm(
            @RequestBody @ApiParam(value="이메일정보 정보", required = trueString email) throws Exception {
 
        String confirm = emailService.sendSimpleMessage(email);
 
        return ResponseEntity.status(200).body(BaseResponseBody.of(200, confirm));
    }
cs

★ 위에서 말했다시피, 인증코드 일치여부 확인을 위해 성공코드와 함께 인증코드 return

 

 

메일 확인

 

 

참고한 블로그

https://badstorage.tistory.com/38

'정리 > Spring' 카테고리의 다른 글

[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09
[Spring Boot + Vue.js] 구글로그인  (0) 2021.08.18

+ Recent posts