JWT 강의를 들으며 실습을 하던 도중 간단하게 정리를 해보고자 한다.
- 헤더
{
"alg": "HS256",
"typ": "JWT"
}
alg : 서명 암호화 알고리즘(ex: HMAC SHA256, RSA)
typ : 토큰 유형
- 페이로드
{
"sub": "1234567890",
"lat": 1516239022
}
iss(issuer; 발행자),
exp(expireation time; 만료 시간),
sub(subject; 제목),
iat(issued At; 발행 시간),
jti(JWI ID)
- 시그니처
HEADER + PAYLOAD + SECRET
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
log.debug("디버그: attemptAuthentication 호출됨");
try {
ObjectMapper om = new ObjectMapper(); //로그인하면 request안에 json이있어서 om이 필요
LoginReqDto loginReqDto = om.readValue(request.getInputStream(), LoginReqDto.class); //LoginReqDto.class타입
//강제 로그인
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginReqDto.getUsername(), loginReqDto.getPassword()); //토큰 생성
//UserDetailService 의 LoadUserByUsername을 호출
//JWT를 쓴다 하더라도 컨트롤러 진입을 하면 시큐리티의 권한체크 인증체크의 도움을 받을 수 있게 세션을 만든다.//인증이 되고 세션은 만들어지자마자
// CustomReponseUtil.success(response, loginRespDto);에 의해 리턴되며 컨트롤러까지 안가고 없어진다 //
// 이세션의 유효기간은 request하고 response하면 끝
Authentication authentication = authenticationManager.authenticate(authenticationToken); //강제 로그인
//Authentication은 authenticationManager 매니저가 필요하다
//강제 로그인
//authentication.getPrincipal() < - LoginUser 객체가 담긴다
return authentication; //successfulAuthentication() 호출
}catch (Exception e){
//unsuccessfulAuthentication 호출함
throw new InternalAuthenticationServiceException(e.getMessage());
}
}
// 로그인 실패
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
CustomReponseUtil.fail(response, "로그인 실패", HttpStatus.UNAUTHORIZED);
}
//return authentication이 잘 작동하면 successfulAuthentication 호출
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.debug("디버그: successfulAuthentication 호출됨");
//로그인이 됐다, 세션이 만들어짐
LoginUser loginUser = (LoginUser) authResult.getPrincipal(); //로그인유저
String jwtToken = JwtProcess.create(loginUser); //jwtToken 토큰 생성
response.addHeader(jwtVo.Header, jwtToken); //토큰을 헤더에 담는다
LoginRespDto loginRespDto = new LoginRespDto(loginUser.getUser()); //유저 정보를 넣어주면된다
CustomReponseUtil.success(response, loginRespDto); //jwt 토큰 필터 구현완료
}
@Transactional //전체 테스트시에 롤백으로 독립적인 테스트를 위해
@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class JwtAuthenticationFilterTest extends DummyObject {
@Autowired
private ObjectMapper om; // JSON으로 변경하기 위해 필요
@Autowired
private MockMvc mvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
public void setUp() throws Exception{
User user = userRepository.save(newUser("saar", "쌀"));
}
@Test //로그인 성공
public void successfulAuthentication_test() throws Exception{
//given
LoginReqDto loginReqDto = new LoginReqDto();
loginReqDto.setUsername("saar");
loginReqDto.setPassword("1234");
String requestBody = om.writeValueAsString(loginReqDto);
System.out.println("테스트 = " + requestBody);
//when
ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON));
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
String jwtToken = resultActions.andReturn().getResponse().getHeader(jwtVo.Header);
System.out.println("테스트 = " + responseBody);
System.out.println("테스트 = " + jwtToken);
//then
resultActions.andExpect(status().isOk());
assertNotNull(jwtToken); //jwt 토큰이 null 이 아니길 기대
assertTrue(jwtToken.startsWith(jwtVo.TOKEN_PREFIX)); //접두사 확인
resultActions.andExpect(jsonPath("$.data.username").value("saar"));//usernam이 saar인지 검증
}
@Test //로그인 실패
public void unsuccessfulAuthentication_test() throws Exception{
//given
LoginReqDto loginReqDto = new LoginReqDto();
loginReqDto.setUsername("saar");
loginReqDto.setPassword("12345");
String requestBody = om.writeValueAsString(loginReqDto);
System.out.println("테스트 = " + requestBody);
//when
ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON));
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
String jwtToken = resultActions.andReturn().getResponse().getHeader(jwtVo.Header);
System.out.println("테스트 = " + responseBody);
System.out.println("테스트 = " + jwtToken);
//then
resultActions.andExpect(status().isUnauthorized());
}
}
//모든 주소에서 동작함(토큰검증) 인가 필터
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
//JWT 토큰 헤더를 추가하지 않아도 해당 필터는 통과 할수는 있지만, 결국 시큐리티 단에서 세션값 검증에 실패
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if(isHeaderVerify(request, response)){//토큰이 존재한다면
//토큰 접두사 제거
String token = request.getHeader(jwtVo.Header).replace(jwtVo.TOKEN_PREFIX,"");
LoginUser loginUser = JwtProcess.verify(token); //토큰검증
//UserDetails 타입 or username) 을 넣을수 있는데 NULL이기떄문에 loginUser = userdetails 통쨰로 넣음
//임시 세션 만들기기//토큰도 있고 검증도 되었으니 인증된 유저로 보면 된다 // 강제로 토큰에 세션을 만든다
Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//강제로 authentication 객체를 생성
SecurityContextHolder.getContext().setAuthentication(authentication); //강제 로그인
//authentication 객체를 SecurityContextHolder 담는다.
//stateless 정책때문에 SecurityContextHolder 에 로그인하면서 loginUser가 저장되더있던게 사라져 있기떄문에 다시 생성하는 과정
//세션에 인증과 권한 체크용으로만 저장을 하고 응답을 하면 사라진다.
// /api/s/hello 면 인증인지 확인만 하면 되고 /api/admin/hello는 권한체크까지 해야하기 때문에
//세션은 브라우저를 안끄거나 로그아웃을 안하면 원래 유지가 됨
//필터를 다 타면 Dispatcher Servlet을 갔다가 컨트롤러로 간다
} //SecurityConfig 의 jwt 필터 등록 필요
chain.doFilter(request, response); // 필터가없으면 다음 필터로 간다
//else로 짰으면 토큰없이는 테스트를 못한다.
// 토큰이 있으면 세션이 만들어지고, 토큰이 없으면 세션이 안만들어지고 컨트롤러로 들어가서 시큐리티가 제어하게끔 했다.
}
//헤더 검증하는 메서드
private boolean isHeaderVerify(HttpServletRequest request, HttpServletResponse response){
String header = request.getHeader(jwtVo.Header);
if (header == null || !header.startsWith(jwtVo.TOKEN_PREFIX)) { //헤더가 널이거나 시작값이 Bearer 이 아니면
return false;
}else{
return true;
}
}
@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class JwtAuthorizationFilterTest {
@Autowired
private ObjectMapper om; // JSON으로 변경하기 위해 필요
@Autowired
private MockMvc mvc;
@Test //doFilterInternal 테스트
public void authorization_success_test() throws Exception{
//given
User user = User.builder().id(1L).role(UserEnum.CUSTOMER).build();
LoginUser loginUser= new LoginUser(user);
String jwtToken = JwtProcess.create(loginUser);//클라이언트가 토큰을 들고 인가를 받기 떄문에 토큰 생성
System.out.println("테스트jwtToken = " + jwtToken);
//when
ResultActions resultActions = mvc.perform(get("/api/s/hello/test").header(jwtVo.Header, jwtToken));
//then
resultActions.andExpect(status().isNotFound());
}
@Test //doFilterInternal 테스트
public void authorization_fail_test() throws Exception{
//given
//when
ResultActions resultActions = mvc.perform(get("/api/s/hello/test"));
//then
resultActions.andExpect(status().isUnauthorized()); //401
}
@Test //doFilterInternal 테스트
public void authorization_admin_test() throws Exception{
//given
User user = User.builder().id(1L).role(UserEnum.CUSTOMER).build();
LoginUser loginUser= new LoginUser(user);
String jwtToken = JwtProcess.create(loginUser);//클라이언트가 토큰을 들고 인가를 받기 떄문에 토큰 생성
System.out.println("테스트jwtToken = " + jwtToken);
//when
ResultActions resultActions = mvc.perform(get("/api/admin/hello/test").header(jwtVo.Header, jwtToken));
//then
resultActions.andExpect(status().isForbidden());
}
}
api/login 하면
1. UPAF 동작 username, password dto로 받는다 로그인 dto를 받는다
2. 파싱해서 loginReqDto object로 바꾼다.
3. 인증 토큰을 만든다// jwt 토큰 x authenticationToken
4. authenticationToken 인증 토큰으로 authenticate() 요청하면
userDetailService의 loadUserByUsername() 가 호출 -username, password DB확인
5. 없으면 unsuccessfulAuthentication() 실행 (컨트롤러까지 도달이 안된상태라),
6. 있으면 단순히 LoginUser 객체 생성하고 리턴
7. 시큐리티 전용 세션에 담김.(전체 세션의 SecurityContextHolder 부분만 Authtication에 LoginUser를 담는다)authentication.getPrincipal()
8. JWT 토큰을 생성하고 response 응답 헤더에 담는다. successfulAuthentication()
response되면 세션에 저장된건 사라진다 stateless정책을 사용해서
클라이언트가 헤더에 JWT를 갖고 있지만 SecurityContextHolder에는 LoginUser 정보가 사라진 상태
api/s/hello / /api/admin/hello
1. BAF 동작, 토큰 검증
2. 토큰이 존재하면 접두사 제거
3. stateless 정책때문에 SecurityContextHolder 에 로그인하면서 loginUser가 저장되더있던게 사라져 있기떄문에 authentication 객체 강제로 생성
4. authentication객체를 SecurityContextHolder에 담아준다
//세션에 인증과 권한 체크용으로만 저장을 하고 응답을 하면 사라진다.
// /api/s/hello 면 인증인지 확인만 하면 되고 /api/admin/hello는 권한체크까지 해야하기 때문에