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
- 필터 동작
- Request 헤더에서 토큰 헤더값을 가져옴
- 토큰만 가지는 PreProcessingToken 객체를 만듦
- PreProcessingToken을 처리하는 Provider를 Provider 매니져에서 찾기
- Provider가 DB 커넥션 없이 인증 처리를 해야함 (JWT의 시그니쳐가 유효한지 체크 -> JWT에서 값 가져오기)
- 인증 과정이 성공하면 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
- 자바스크립트 SDK를 활용해서 클라이언트 프로그래머가 OAuth2 Autorization Server에 액세스 토큰 요청을 함
- 사용자가 해당 공급자의 서비스 로그인을 한 후 액세스 토큰을 받아옴 : json
- 어플리케이션 서버로 요청할 때 해당 액세스 토큰을 실어서 보내줌 : HTTP 헤더(어딘가에 토큰을 저장해뒀다가 인증이 필요한 서비스 콜할 때)
- 어플리케이션 서버는 해당 OAuth 서버(공급자)에 액세스 토큰 확인을 함 : 너희가 발급해준 것이 맞느냐(액세스 토큰 검증 과정을 거쳐야함)
- 맞으면 JWT 발급
고려해야할 것
- 소셜 공급자가 제공해주는 정보 중에서 가져올 것을 정해야함 : 모든 정보가 서비스에서 필요하지않기 때문에 - 많은 개인정보를 저장하지않도록 할 것
- 공급자에 따라 뺴올 정보를 다르게 하는 것보다 유니버셜하게 사용하도록 기준을 정할 것 : 각각 다르게 뺴오면 데이터베이스 설계할 떄 힘들어짐
- 소셜 공급자가 하나가 아니라 여러 소셜 공급자가 있다는 것
구현하기
- 필터, 프로바이더, 핸들러, 인증 전 후 객체를 만들어야함 : 인증 전 객체는 각각 따로 만드는 것이 좋음 - 공통적으로 사용하는 인증 전 객체라면 모든 필터에 걸릴 수 있음
flow
- 클라이언트가 인증서버에 요청해서 액세스 토큰을 받아옴
- 클라이언트가 서버로 인증이 필요한 요청을 할 때(최초?) 프로바이더(Provider)와 액세스 토큰(token)를 전달해줌 - 약속(key - value 모두 약속)
- 서버는 액세스 토큰을 받아서 RestTemplate로 인증 서버에 요청해서 정보를 받아옴 - 소셜 인증은 이미 되었다는 전제 하에 진행됨(클라이언트에서 처리하는 것으로 합의를 봤다는 전제 하에 진행)
- 인증서버로부터 데이터(사용자 정보 - 아래 Json 데이터)를 받아와서 파싱 후 데이터베이스에 이전에 인증을 받아서 데이터베이스에 이미 기록되어있는지 물어봄 (업데이트 진행해도 되지않나?) 아예 기록이 없으면 데이터베이스에 기록해둠
- 인증과정이 모두 성공했으면(프로바이더 처리 성공) JWT 발급
- 이후부터는 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는 인증되었다는 것과 서버에서 필요한 부가 정보를 담을 수 있음