쵼쥬
쵼쥬의 개발공부 TIL
쵼쥬
전체 방문자
오늘
어제
  • 분류 전체보기 (276)
    • 코딩테스트 (192)
      • [알고리즘] 알고리즘 정리 (7)
      • [백준] 코딩테스트 연습 (126)
      • [프로그래머스] 코딩테스트 연습 (59)
    • Spring (71)
      • [인프런] 스프링 핵심 원리- 기본편 (9)
      • [인프런] 스프링 MVC 1 (6)
      • [인프런] 스프링 MVC 2 (4)
      • [인프런] 실전! 스프링 부트와 JPA 활용1 (7)
      • [인프런] 실전! 스프링 부트와 JPA 활용2 (5)
      • [인프런] 실전! 스프링 데이터 JPA (7)
      • [인프런] 실전! Querydsl (7)
      • JWT (5)
      • [인프런] Spring Cloud (17)
      • [인프런] Spring Batch (4)
    • Java (6)
      • [Java8] 모던인자바액션 (4)
      • [부스트코스] 웹 백엔드 (2)
      • [패스트캠퍼스] JAVA STREAM (0)
    • CS (6)
      • 디자인 패턴과 프로그래밍 패터다임 (2)
      • 네트워크 (4)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • spring
  • 스프링
  • 부스트코스
  • jpa
  • 코딩테스트
  • 백분
  • 누적합
  • 백준
  • 자바
  • 타임리프
  • querydsl
  • 알고리즘
  • Spring Data JPA
  • 위클리 챌린지
  • 비트마스킹
  • 구현
  • 인프런
  • 프로그래머스
  • MVC
  • BFS

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
쵼쥬
Spring/[인프런] Spring Cloud

Users Microsservice - Login

Spring/[인프런] Spring Cloud

Users Microsservice - Login

2022. 6. 20. 14:13

Users Microsservice - Login

기능 URI(API Gateway 사용시) URI(API Gateway 미사용시) HTTP Method
사용자 로그인 /user-service/login /login POST

요청을 서버에 보내면 -> header에 토큰과 userId 반환

 

AuthenticationFilter.java - Spring Security를 이용한 로그인 요청 발생시 작업을 처리해주는 Custom Filter 클래스

attemptAuthentication() : 사용자로부터 전달받은 데이터를 추출해서 토큰에 저장하고 인증처리

successfulAuthentication() : 인증이 되었을때 어떻게 처리해줄지 

package com.example.userservice.security;

import com.example.userservice.vo.RequestLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        try {
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

            // 사용자의 요청을 토큰으로 바꿔서 manager에게 전달하면 비교해서 인증
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getEmail(),
                            creds.getPassword(),
                            new ArrayList<>()
                    )
            );

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 인증 성공시 처리
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
//        super.successfulAuthentication(request, response, chain, authResult);
    }
}

 

WebSecurity.java - 사용자 요청에 대해 AuthenticationFilter를 거치도록 수정

package com.example.userservice.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.servlet.Filter;

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    // 여러 configure 중 권한 관련
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
//        http.authorizeRequests().antMatchers("/users/**").permitAll();
        http.authorizeRequests().antMatchers("/**")
                .hasIpAddress("") // ip 변경 필요
                .and()
                .addFilter(getAuthenticationFilter());

        // h2에선 데이터가 프레임 별로 나눠져 있는데 그걸 무시해야 h2-console 사용가능
        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        authenticationFilter.setAuthenticationManager(authenticationManager());

        /**
         * AuthenticationManager는 ProvierManager를 구현한 클래스로써, 
         * 인자로 전달받은 유저에 대한 인증 정보를 담고 있으며, 해당 인증 정보가 유효할 경우 
         * UserDetailsService에서 적절한 Principal을 가지고 있는 
         * Authentication 객체를 반환해 주는 역할을 하는 인증 공급자(Provider) 입니다.
         */
        
        return authenticationFilter;
    }
}

 

UserDetailsService 등록 - 기존의 UserService.java를 활용

public interface UserService extends UserDetailsService {
}
package com.example.userservice.service;

import com.example.userservice.dto.UserDto;
import com.example.userservice.jpa.UserEntity;
import com.example.userservice.jpa.UserRepository;
import com.example.userservice.vo.ResponseOrder;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
public class UserServiceImpl implements UserService {

    UserRepository userRepository;
    BCryptPasswordEncoder passwordEncoder;

    @Autowired
    public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }


    @Override
    public UserDto createUser(UserDto userDto) {
        userDto.setUserId(UUID.randomUUID().toString());

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);   // 속성이 모두 맞아 떨어지지 않으면 안되게 세팅
        UserEntity userEntity = mapper.map(userDto, UserEntity.class);
        userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));

        userRepository.save(userEntity);

        UserDto returnUserDto = mapper.map(userEntity, UserDto.class);

        return returnUserDto;
    }

    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        List<ResponseOrder> orders = new ArrayList<>();
        userDto.setOrders(orders);

        return null;
    }

    @Override
    public Iterable<UserEntity> getUserByAll() {
        return userRepository.findAll();
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username);

        if (userEntity == null) {
            throw new UsernameNotFoundException(username);  // 사용자가 검색되지 않았을때 security에서 예외 제공
        }

        // security 에 포함되어 있는 user
        // 이메일, 비밀번호 다 검증이 완료 되면 사용자 정보반환하는 클래스
        // 추가 옵션
        // 마지막 ArrayList 로그인 되었을때 그 다음하는 작업 중 권한을 추가할 수 있음. 여기선 일단 없음
        return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
                true, true, true, true,
                new ArrayList<>());
    }
}

 

API Gateway Service 수정 - User Service 에 대한 Routes 정보 수정

filters - RewritePath = /user-serivce/(?<segment>.*), /$\{segment} -> 사용자가 전달한 경로를 수정해서 마이크로 서비스에 전달

application.yml 수정

- id: user-service
  uri: lb://USER-SERVICE
  predicates:
    - Path=/user-service/login
    - Method=POST
  filters:
    - RemoveRequestHeader=Cookie  # POST 로 요청된 데이터는 매번 새로운 요청으로 인식하기 위해 header 초기화
    - RewritePath=/user-service/(?<segment>.*), /$\{segment} # 사용자가 전달한 경로를 수정해서 마이크로 서비스에 전달
- id: user-service
  uri: lb://USER-SERVICE
  predicates:
    - Path=/user-service/users
    - Method=POST
  filters:
    - RemoveRequestHeader=Cookie  
    - RewritePath=/user-service/(?<segment>.*), /$\{segment}
- id: user-service
  uri: lb://USER-SERVICE
  predicates:
    - Path=/user-service/**
    - Method=GET
  filters:
    - RemoveRequestHeader=Cookie  
    - RewritePath=/user-service/(?<segment>.*), /$\{segment}

API Gateway에서 변경된 uri로 마이크로서비스에게 전달하기 때문에 RequestMapping 수정 필요

 

Routes 테스트 

user-service를 디버깅으로 실행해서 확인

login은 만들지 않았지만 Spring Security를 사용하면 기본적으로 제공

 

 

로그인 처리 과정

사용자 요청이 AuthenticationFilter 로 전달되서 attemptAuthentication메서드가 처리

-> UsernamePasswordAuthenticationToken으로 바꿔서 처리

-> UserDetailService를 구현하고 있는 클래스에서 loadUserByUsername() 메서드 실행 (DB 에서 가져온 사용자 정보를 User 객체로 변환해서 사용)

-> 정상적으로 인증되었으면 successfulAuthentication 에서 JWT 토큰을 발행 (userId로 토큰을 만들기 위해 email을 가지고 userId를 가지고 옴)

-> 토큰을 responseHeader 에 저장해서 사용자에게 반환

 

 

로그인 성공 처리

AuthenticationFilter.java의 successfulAuthentication() 수정 - 인증 성공 시 사용자에게 Token 발행

private UserService userService;
private Environment env;

public AuthenticationFilter(AuthenticationManager authenticationManager, UserService userService, Environment env) {
    super.setAuthenticationManager(authenticationManager);
    this.userService = userService;
    this.env = env;
}

@Override
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    String userName = ((User) authResult.getPrincipal()).getUsername();
    UserDto userDetails = userService.getUserDetailsByEmail(userName);
}

WebSecurity

    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), userService, env);

        // 생성자를 통해 AuthenticationManager 전달했기 때문에 아래 코드 필요 없음
//        authenticationFilter.setAuthenticationManager(authenticationManager());

        /**
         * AuthenticationManager는 ProvierManager를 구현한 클래스로써,
         * 인자로 전달받은 유저에 대한 인증 정보를 담고 있으며, 해당 인증 정보가 유효할 경우
         * UserDetailsService에서 적절한 Principal을 가지고 있는
         * Authentication 객체를 반환해 주는 역할을 하는 인증 공급자(Provider) 입니다.
         */

        return authenticationFilter;
    }

WebSecurity.java의 configure 수정 - Userservice 사용할 수 있도록 메소드 수정

 

 

Userservice, UserserviceImpl, UesrRepository - 사용자 검색 메서드 추가

UserServiceImpl

@Override
public UserDto getUserDetailsByEmail(String email) {
    UserEntity userEntity = userRepository.findByEmail(email);

    if (userEntity == null) {
        throw new UsernameNotFoundException(email);
    }

    UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
    return userDto;
}

 

 

JWT(Json Web Token)

JWT 생성

pom.xml

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

 

application.yml

token:
  expiration_time: 86400000 # 토큰 지속 시간 (하루 24 * 60 * 60 * 1000)
  secret: user_token # 임의의 값

 

AuthenticationFilter.java의 successfulAuthentication() 수정 - 인증 성공 시 사용자에게 Token 발행

@Override
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    String userName = ((User) authResult.getPrincipal()).getUsername();
    UserDto userDetails = userService.getUserDetailsByEmail(userName);

    String token = Jwts.builder()
            .setSubject(userDetails.getUserId()) // 토큰에 들어갈 내용
            .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) // 토큰 시간
            .signWith(SignatureAlgorithm.ES512, env.getProperty("token.secret")) // 암호화
            .compact();

    response.addHeader("token", token);
    response.addHeader("userId", userDetails.getUserId());
}

 

 

JWT 처리 과정

 

전통적인 인증 시스템

username, password 전달해서 인증 요청

-> 서버는 DB 확인해서 session 반환

-> cookie 를 이용해서 서비스 사용

-> 서버는 seesion 정보 확인해서 서비스 결과 반환

 

문제점 

 - 세션과 쿠키는 모바일 어플리케이션에서 유효하게 사용할 수 없다. (공유 불가) 예> React에서도 안됨

 - 렌더링된 HTML 페이지가 반환되지만, 모바일 어플링케이션에서는 JSON(or XML) 과 같은 포맷 필요

 

Token 기반 인증 시스템

username, password 전달해서 인증 요청

-> 서버는 session을 발급하는 것이 아닌 Token을 발금

-> 발급받은 Token(Bearer Token)을 전달해서 서비스 사용

-> 서버는 Token 검증해서 서비스 결과 반환

 

JWT 지원 : NodeJS(Vue, React), PHP, java, Ruby, .NET, Python..

 

장점 

 - 클라이언트 독립적인 서비스(stateless)

 - CDN

 - No Cookie-Session(No CSRF, 사이트간 요청 위조)

 - 지속적인 토큰 저장

 

분산된 서버에서 공유 가능 (DB에 발급된 토큰을 저장해서 여러 서버에서 공유 가능)

 

 

API Gateway service - AuthorizationHeaderFilter

Spring security와 JWT token 사용 추가

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

<!--        java.lang.NoClassDefFoundError: Could not initialize class javax.xml.bind.DatatypeConverterImpl-->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
        </dependency>

 

AuthoriztionHeaderFilter.java 추가

package com.example.apigatewayservice.filter;

import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {

    Environment env;

    @Autowired
    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    public static class Config {

    }

    // login -> token -> users (with token) -> header (include token)
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // 헤더에 AUTHORIZATION 있는지 확인
            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
            }

            // 헤더에서 Bearer 토큰 추출
            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace("Bearer", "");
            if (!isJwtValid(jwt)) {
                return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);
        };
    }

    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;

        String subject = null;

        try {
            // 토큰에서 넣었던 sub 추출
            subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
                    .parseClaimsJws(jwt).getBody()
                    .getSubject();
        } catch (Exception ex) {
            returnValue = false;
        }

        if (subject == null || subject.isEmpty()) {
            returnValue = false;
        }

        return returnValue;
    }


    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        log.error(err);

        return response.setComplete();
    }
}

 

application.yml

- id: user-service
  uri: lb://USER-SERVICE
  predicates:
    - Path=/user-service/**
    - Method=GET
  filters:
    - RemoveRequestHeader=Cookie
    - RewritePath=/user-service/(?<segment>.*),/$\{segment}
    - AuthorizationHeaderFilter
    
    
token:
  secret: user_token # 임의의 값

 

Postman Authorization 추가

Bearer Authentication : 

API 에 접속하기 위해서는 access token 을 API 서버에 제출해서 인증 처리

OAuth를 위해서 고안된 방법, RFC 6750

 

'Spring > [인프런] Spring Cloud' 카테고리의 다른 글

Spring Cloud Bus  (0) 2022.06.24
Configuration Service  (0) 2022.06.24
Catalogs, Orders Microservice  (0) 2022.06.19
Users Microservice  (0) 2022.06.18
E-commerce 어플리케이션  (0) 2022.06.18
  • Users Microsservice - Login
  • JWT(Json Web Token)
  • API Gateway service - AuthorizationHeaderFilter
'Spring/[인프런] Spring Cloud' 카테고리의 다른 글
  • Spring Cloud Bus
  • Configuration Service
  • Catalogs, Orders Microservice
  • Users Microservice
쵼쥬
쵼쥬

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.