TIL - 0721

SQL

JOIN

  • 두 개의 테이블에 대한 쿼리 실행을 할 떄 사용됨 - PK, FK를 활용해서 JOIN
  • 여러 타입의 JOIN이 있음
    • Inner Join : ON으로 지정한 결합 조건에 일치하는 두 테이블의 행만을 가져오는 것 - 어디 테이블도 다 가지고 오는게 아니라 딱 공통되는 행만
    • Outer Join : 방향에 따라서(테이블을 쿼리문에서 어디에 놓느냐 - LEFT / RIGHT) 기준이 되는 테이블의 행은 모두 가져오고, 이외 테이블의 행은 ON 결합 조건에 해당하는 행만 가져옴, 나머지 데이터는 NULL로 처리됨(기준 행의 FK 또한 일치하지않으면 NULL로)
  • 테이블 JOIN을 할 때 ON 결합 조건을 항상 포함시켜야함 : 실제로 결합시키는게 아니라 같이 가져와서 보여줄 때만 결합형태로 보여줌
  • 직접 해보기 : Orders, Customers 테이블 생성 - JOIN 쿼리 날려보기
/* 생성 결과 */
> DESC Orders;
+---------+------------+------------+---------------------+-----------+
| OrderID | CustomerID | EmployeeID | OrderDate           | ShipperID |
+---------+------------+------------+---------------------+-----------+
|   10308 |          2 |          7 | 1996-09-18 00:00:00 |         3 |
|   10309 |         37 |          3 | 1996-09-19 00:00:00 |         1 |
|   10310 |         77 |          8 | 1996-09-20 00:00:00 |         2 |
+---------+------------+------------+---------------------+-----------+

> DESC Customers;
+--------------+-------------+------+-----+---------+----------------+
| Field        | Type        | Null | Key | Default | Extra          |
+--------------+-------------+------+-----+---------+----------------+
| CustomerID   | int(11)     | NO   | PRI | NULL    | auto_increment |
| CustomerName | varchar(30) | YES  |     | NULL    |                |
| ContactName  | varchar(30) | YES  |     | NULL    |                |
| Address      | varchar(40) | YES  |     | NULL    |                |
| City         | varchar(20) | YES  |     | NULL    |                |
| PostalCode   | int(11)     | YES  |     | NULL    |                |
| Country      | varchar(15) | YES  |     | NULL    |                |
+--------------+-------------+------+-----+---------+----------------+

/* Inner JOIN */
> SELECT Orders.OrderID, Customers.CustomerName FROM Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID;
+---------+------------------------------------+
| OrderID | CustomerName                       |
+---------+------------------------------------+
|   10308 | Ana Trujillo Emparedados y helados |
+---------+------------------------------------+

/* OUTER JOIN - LEFT */
> SELECT * FROM Orders LEFT JOIN Customers ON Orders.CustomerID = Customers.CustomerID ORDER BY Orders.CustomerID;
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+--------------+--------------------------------+--------------+------------+---------+
| OrderID | CustomerID | EmployeeID | OrderDate           | ShipperID | CustomerID | CustomerName                       | ContactName  | Address                        | City         | PostalCode | Country |
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+--------------+--------------------------------+--------------+------------+---------+
|   10308 |          2 |          7 | 1996-09-18 00:00:00 |         3 |          2 | Ana Trujillo Emparedados y helados | Ana Trujillo | Avda. de la Constitución 2222  | México D.F.  |       5021 | Mexico  |
|   10309 |         37 |          3 | 1996-09-19 00:00:00 |         1 |       NULL | NULL                               | NULL         | NULL                           | NULL         |       NULL | NULL    |
|   10310 |         77 |          8 | 1996-09-20 00:00:00 |         2 |       NULL | NULL                               | NULL         | NULL                           | NULL         |       NULL | NULL    |
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+--------------+--------------------------------+--------------+------------+---------+

/* OUTER JOIN - RIGHT */
> SELECT * FROM Orders RIGHT JOIN Customers ON Orders.CustomerID = Customers.CustomerID ORDER BY Customers.CustomerID;
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+----------------+--------------------------------+--------------+------------+---------+
| OrderID | CustomerID | EmployeeID | OrderDate           | ShipperID | CustomerID | CustomerName                       | ContactName    | Address                        | City         | PostalCode | Country |
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+----------------+--------------------------------+--------------+------------+---------+
|    NULL |       NULL |       NULL | NULL                |      NULL |          1 | Alfreds Futterkiste                | Maria Anders   | Obere Str. 57                  | Berlin       |      12209 | Germany |
|   10308 |          2 |          7 | 1996-09-18 00:00:00 |         3 |          2 | Ana Trujillo Emparedados y helados | Ana Trujillo   | Avda. de la Constitución 2222  | México D.F.  |       5021 | Mexico  |
|    NULL |       NULL |       NULL | NULL                |      NULL |          3 | Antonio Moreno Taquería            | Antonio Moreno | Mataderos 2312                 | México D.F.  |       5023 | Mexico  |
+---------+------------+------------+---------------------+-----------+------------+------------------------------------+----------------+--------------------------------+--------------+------------+---------+

spring-security JWT Authentication 개발하기

  • 위의 인증 과정을 통해 클라이언트는 JWT를 응답받음 : 만료 기간까지 해당 JWT를 가지고 사용자 인증을 할 것
    • 서버는 무상태이기때문에 JWT(String)를 전달하면서 자신이 사용자임을 인증하는 것
    • JWTProvider에서 데이터베이스나 인증서버에 다시 한번 요청하는 커넥션을 생성하지않는 것이 핵심

JWT Authentication flow

  1. 필터 동작
  2. Request 헤더에서 토큰 헤더값을 가져옴
  3. 토큰만 가지는 PreProcessingToken 객체를 만듦
  4. PreProcessingToken을 처리하는 Provider를 Provider 매니져에서 찾기
  5. Provider가 DB 커넥션 없이 인증 처리를 해야함 (JWT의 시그니쳐가 유효한지 체크 -> JWT에서 값 가져오기)
  6. 인증 과정이 성공하면 PostAuthentication 객체를 생성하고, 아니라면 예외(처리는 또 따로 - ControllerAdvice에서 일괄처리하던지)

토큰값 추출하고 AccountContext 생성하기 : JwtFactory -> JwtManager로 변경

  • 데이터베이스 조회없이 서버에서 인증해준 것이 맞는지 체크할 수 있음
    • JWT 생성 시 secret key와 알고리즘이 일치해야 decode가 정상적으로 되고, 아니면 익셉션 발생
  • 헤더에 기록된 토큰 -> 토큰에 기록되어있는 정보 가져오기
Authorization: Bearer your_token_value
  • verify -> decode
public AccountContext decodeJWT(String token) {
    AccountContext accountContext = null;
    JWTVerifier verifier = JWT.require(getAlgorithm()).build();
    try {
        DecodedJWT decodedJWT = verifier.verify(token);
        accountContext = AccountContext.fromJWTClaim(
            decodedJWT.getClaim(USER_ID).asString(),
            "",
            decodedJWT.getClaim(USER_ROLE).asString()
        );
    } catch (JWTVerificationException e) {
        log.error("jwt verify exception");
    }
    return accountContext;
}

필터 생성 및 등록하기

  • 필터는 특정 URL에서 처리하도록 해야하므로 이를 담당할 객체를 만들어야함 : FilterMatcher(처리해야할 패스와 처리하지말아야할 패스를 받아서)
    • Ant style Pattern : 와일드카드 문자(?, *, **)를 사용하는 패턴 : “/api/~~”는 검증을 해야할 부분, “/login”은 인증을 해야하는 패스라 필터 스킵 패스
  • successfulAuthentication 메소드 : 인증 성공 후 생성된 PostAuthentication 객체를 SecurityContext에 실어주고, 다음 체인 과정을 진행하도록 함
    • 사용자 인증 완료 라는 하나의 배경을 Request 진행 배경에 설정하는 것
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private JwtAuthenticationFailureHandler failureHandler;

    private JwtAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }

    public static JwtAuthenticationFilter createFilter(RequestMatcher matcher, JwtAuthenticationFailureHandler failureHandler) {
        JwtAuthenticationFilter token = new JwtAuthenticationFilter(matcher);
        token.failureHandler = failureHandler;
        return token;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        JwtPreAuthenticationToken preAuthToken = JwtPreAuthenticationToken.fromJwt(JwtTokenHeaderParser.parse(request));
        return super.getAuthenticationManager().authenticate(preAuthToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authResult);
        SecurityContextHolder.setContext(securityContext);
        chain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        failureHandler.onAuthenticationFailure(request, response, failed);
    }
}

/* 필터 등록하기 */
private JwtAuthFilter jwtAuthFilter() throws Exception {
    JwtAuthFilterMatcher filterMatcher = new JwtAuthFilterMatcher("/api/**", Arrays.asList("/login");
    JwtAuthFilter filter = new JwtAuthFilter(filterMatcher, failureHandler);   
    return filter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    http.csrf().disable();
    http.headers().frameOptions().disable();
    http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}

프로바이더 생성하기

  • JWT를 verify - decode함 : verify 과정에서 인증이 완료되고, decode뒤 claim set에 저장된 부가정보를 통해서 추가적인 처리를 할 수 있음
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private JwtManager jwtManager;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtPreAuthenticationToken preAuthToken = (JwtPreAuthenticationToken) authentication;
        AccountContext accountContext = jwtManager.decodeJWT(preAuthToken.getToken());
        return PostAuthenticationToken.getPostToken(accountContext);
    }

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

spring-security 소셜 회원가입 - 로그인 구현

회원가입

  • 최초 혹은 JWT가 아닌 액세스 코드 만으로 로그인 시 데이터베이스에 유저의 정보를 일부 기록해둠

로그인

  • 서버에서는 서비스 프로바이더에 확인 요청을 한 후 JWT를 발급해서 만료 기간 전까지는 인증 서버로 다시 가지 않도록 함

Implicit Grant Flow

  1. 자바스크립트 SDK를 활용해서 클라이언트 프로그래머가 OAuth2 Autorization Server에 액세스 토큰 요청을 함
  2. 사용자가 해당 공급자의 서비스 로그인을 한 후 액세스 토큰을 받아옴 : json
  3. 어플리케이션 서버로 요청할 때 해당 액세스 토큰을 실어서 보내줌 : HTTP 헤더(어딘가에 토큰을 저장해뒀다가 인증이 필요한 서비스 콜할 때)
  4. 어플리케이션 서버는 해당 OAuth 서버(공급자)에 액세스 토큰 확인을 함 : 너희가 발급해준 것이 맞느냐(액세스 토큰 검증 과정을 거쳐야함)
  5. 맞으면 JWT 발급

고려해야할 것

  • 소셜 공급자가 제공해주는 정보 중에서 가져올 것을 정해야함 : 모든 정보가 서비스에서 필요하지않기 때문에 - 많은 개인정보를 저장하지않도록 할 것
    • 공급자에 따라 뺴올 정보를 다르게 하는 것보다 유니버셜하게 사용하도록 기준을 정할 것 : 각각 다르게 뺴오면 데이터베이스 설계할 떄 힘들어짐
  • 소셜 공급자가 하나가 아니라 여러 소셜 공급자가 있다는 것

구현하기

  • 필터, 프로바이더, 핸들러, 인증 전 후 객체를 만들어야함 : 인증 전 객체는 각각 따로 만드는 것이 좋음 - 공통적으로 사용하는 인증 전 객체라면 모든 필터에 걸릴 수 있음

flow

  1. 클라이언트가 인증서버에 요청해서 액세스 토큰을 받아옴
  2. 클라이언트가 서버로 인증이 필요한 요청을 할 때(최초?) 프로바이더(Provider)와 액세스 토큰(token)를 전달해줌 - 약속(key - value 모두 약속)
  3. 서버는 액세스 토큰을 받아서 RestTemplate로 인증 서버에 요청해서 정보를 받아옴 - 소셜 인증은 이미 되었다는 전제 하에 진행됨(클라이언트에서 처리하는 것으로 합의를 봤다는 전제 하에 진행)
  4. 인증서버로부터 데이터(사용자 정보 - 아래 Json 데이터)를 받아와서 파싱 후 데이터베이스에 이전에 인증을 받아서 데이터베이스에 이미 기록되어있는지 물어봄 (업데이트 진행해도 되지않나?) 아예 기록이 없으면 데이터베이스에 기록해둠
  5. 인증과정이 모두 성공했으면(프로바이더 처리 성공) JWT 발급
  6. 이후부터는 JWT를 사용해서 인증하는 절차를 거침 - 위는 소셜 회원가입 / 로그인

사용자 정보 가져오기

  • 카카오 로그인 API(OAuth2 Authorization Server API) 요청 메세지 요구사항
GET/POST /v2/user/me HTTP/1.1
Host: kapi.kakao.com
Authorization: Bearer {access_token}
Content-type: application/x-www-form-urlencoded;charset=utf-8
  • API 충족 시 리턴되는 데이터
    • 해당 데이터를 파싱해서 데이터베이스에 요청을 하고, 이미 있는 정보일 경우 JWT 발행, 없는 정보일 경우 데이터베이스에 기록한 후 JWT 발행
    • Jackson json 데이터 파싱 : @JsonProperty로 API 문서에 있는 key값을 입력해줘야함
    • API 사용 설정부터 하자 : 권한 체크하고 API 사용 설정 후 실 테스트 해보기 - 제공되는 데이터에 따라 AccountContext(DTO) 관리 정보가 달라짐(프로바이더마다 어떤 정보를 제공하는지 보고 공통되는 정보를 파악하기)
    • 카카오 API는 정말 친절하게 되어있음 : 툴까지 만들어둠
HTTP/1.1 200 OK
{
  "id":123456789,
  "properties":{
     "nickname":"홍길동",
     "thumbnail_image":"http://xxx.kakao.co.kr/.../aaa.jpg",
     "profile_image":"http://xxx.kakao.co.kr/.../bbb.jpg",
     "custom_field1":"23",
     "custom_field2":"여"
     ...
  },
  "kakao_account": { 
    "has_email": true, 
    "is_email_valid": true,   
    "is_email_verified": true,   
    "email": "xxxxxxx@xxxxx.com"
    "has_age_range":true,
    "age_range":"20~29",
    "has_birthday":true,
    "birthday":"1130",
    "has_gender":true,
    "gender":"female"
  }
}

액세스 토큰과 JWT

  • 액세스 토큰은 말그대로 OAuth 프로바이더에 API 요청을 할 때 필요한 액세스 토큰이고, JWT는 인증되었다는 것과 서버에서 필요한 부가 정보를 담을 수 있음