기존 Access Token만 사용했던 코드에서 Refresh Token도 사용해보도록 바꿔봤음.
// access token create
public String createToken(Authentication authentication) {
// 권한 가져오기
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim("roles", authorities)
.setIssuedAt(new Date(System.currentTimeMillis())) // 토큰 발행 시간 정보
.setExpiration(new Date(System.currentTimeMillis() + expiration)) // 유효시간 저장 -> ms단위
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘, signature에 들어갈 secret값 세팅
.compact();
}
// refresh token create
public String createRefreshToken() {
return Jwts.builder()
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
Refresh Token은 Access Token 재발급용으로 사용할 것이기 때문에 별다른 정보를 넣지않음.
public TokenDTO login(String student_id, String password) {
// User 검증
Optional<User> optionalUser = Optional.ofNullable(userRepository.findByStudent_id(student_id).orElseThrow(() ->
new BadCredentialsException("존재하지 않는 계정입니다.")));
if (!bCryptPasswordEncoder.matches(password, optionalUser.get().getPassword())) {
throw new BadCredentialsException("비밀번호가 일치하지 않습니다..");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(optionalUser.get().getStudent_id(), optionalUser.get().getPassword());
String accessToken = jwtProvider.createToken(authentication);
// refresh
String refreshToken = jwtProvider.createRefreshToken();
Refresh refresh = new Refresh();
refresh.setStudentId(optionalUser.get().getStudent_id());
refresh.setRefresh(refreshToken);
refresh.setDate(timeNow());
updateRefreshToken(optionalUser.get().getStudent_id(), refresh);
return TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
로그인을 하면 Authentication 객체를 생성하여 파라미터를 보내주고 refreshToken은 DB에 따로 저장.
@PostMapping("/loginForm")
public ResponseEntity<TokenDTO> login(@RequestBody @Valid LoginRequestDTO loginRequestDTO, HttpServletResponse response) {
TokenDTO token = userService.login(loginRequestDTO.getStudent_id(), loginRequestDTO.getPassword());
if (token != null){
log.info("로그인 POST 1 [성공]");
Cookie accessCookie = new Cookie("access", token.getAccessToken());
Cookie refreshCookie = new Cookie("refresh", token.getRefreshToken());
accessCookie.setHttpOnly(true);
accessCookie.setMaxAge(3600); // 초
accessCookie.setPath("/");
response.addCookie(accessCookie);
refreshCookie.setHttpOnly(true);
// refreshCookie.setMaxAge();
refreshCookie.setPath("/");
response.addCookie(refreshCookie);
return new ResponseEntity<>(new TokenDTO(token.getAccessToken(), token.getRefreshToken()), HttpStatus.OK);
}
else {
log.info("로그인 POST 2 [실패]");
// 401에러
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new TokenDTO("ID 또는 암호에 오류가 있습니다.",""));
}
}
두 개의 토큰 모두 쿠키에 저장하였고 -> (애초에 둘 다 쿠키에 넣는게 맞는지도 공부해봐야 함)
일단 Controller에서 쿠키 저장을 했는데 어디에서 구현하는지에 대해서는 알아보고 더 좋은 방법으로 사용하도록 바꿀 예정.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 토큰 추출
String AccessToken = resolveToken(request);
// token의 유효성 검사
if (AccessToken != null && "ACCESS".equals(jwtProvider.validateToken(AccessToken))) {
Authentication authentication = jwtProvider.getAuthentication(AccessToken);
// 권한부여
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("권한 부여 성공");
} else if (AccessToken != null && "EXPIRED".equals(jwtProvider.validateToken(AccessToken))) { // access token 만료
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Error
response.getWriter().write("Access Token이 만료되었습니다.");
log.info("Access Token이 만료되었습니다.");
}
filterChain.doFilter(request, response);
}
validateToken()에서는 token을 파라미터로 받아 해당 토큰의 유효성을 확인한 뒤
정상적인 토큰이라면 "ACCESS"라는 문자열을 반환하고
만료되었다면 "EXPIRED"라는 문자를 반환하도록 만들었고
만약 만료되었을 시 상태코드에 401에러를 발생.
@PostMapping("/reissue")
public ResponseEntity<String> reissue(HttpServletResponse response,@RequestBody Map<String, String> refresh) {
String token = refresh.get("refresh"); // refresh token
String newAccessToken = userService.reissueToken(token);
if (newAccessToken == "401" || newAccessToken == null) { // 401 -> refreshToken 만료 , null -> 다른 에러
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("오류가 발생했습니다. 다시 로그인해주세요");
}
Cookie accessCookie = new Cookie("access", newAccessToken);
accessCookie.setHttpOnly(true);
accessCookie.setMaxAge(3600); // 초
accessCookie.setPath("/");
response.addCookie(accessCookie);
return ResponseEntity.ok(newAccessToken);
}
public String reissueToken(String token) {
if (jwtProvider.validateToken(token) == "EXPIRED") { // refresh token의 토큰 만료
log.info("Refresh Token도 만료됐다는데??");
return "401";
} else if (jwtProvider.validateToken(token) == "ACCESS") { // refresh token 토큰 유효
String student_id = refreshRepository.findByRefresh(token).getStudentId();
String password = userRepository.findByStudent_id(student_id).get().getPassword();
Authentication authentication = new UsernamePasswordAuthenticationToken(student_id, password);
String accessToken = jwtProvider.createToken(authentication);
return accessToken;
}
log.info("UserSerive null 에러");
return null;
}
프론트에서는 401에러를 받을 시 controller를 통해 reissueToken()함수를 실행시키고
refreshToken도 만료되었을 시 401에러를,
유효하다면 인증객체를 만든 뒤 새로운 accessToken을 만들어 반환.
Controller에서는 반환받은 accessToken의 반환값을 받아 예외처리 한 뒤 정상이면 쿠키에 다시 추가.
-> setMaxage, setPath등 쿠키 설정하는 것에 대해서 더 알아봐야 할듯???
'
'
'
'
'
'
학교 다니면서 혼자 이것저것 만들어보려니까 시간이 너무 오래 걸렸지만 다 하고 나니까 너무 개운하다.
같이 프로젝트하기로 한 친구랑 DB를 같이 사용해야해서
AWS/RDS먼저 하고 드디어 이제 서비스 로직 구현한다.. 로그인만 몇개월 동안 잡고있었나....
'
'
'
'
현재 프로젝트에서는 AccessToken만 사용하여 로그인하도록 변경하였습니다.