- 클라이언트가 로그인 정보와 함께 로그인 요청을 보낸다.
- 서버는 가입한 유저인지 유저정보로 확인한다.
- 유저가 확인이 된다면, 토큰(access,refresh)을 생성하고 발급한다.
- 클라이언트는 발급받은 토큰으로 로그인 유지, 유저 정보 불러오기, 유저인증 수단으로 활용한다.
- 만약 access 토큰이 만료 될 경우 클라이언트는 refresh토큰과 함께 재발급 요청을 보낸다.
- 클라이언트로부터 받은 refreshtoken이 유효할 경우 서버는 다시 토큰(access, refresh)을 갱신하여 재발급한다.
- 토큰이 유효할 경우 서비스를 이용하고
- 토큰이 만료되거나 조작 시에 프론트와 약속된 예외 처리를 해야 한다.
jwt :
secret : PUT SECRET_KEY
@RestController // @Controller + @ResponseBody
@RequiredArgsConstructor //생성자 주입
@RequestMapping("/user")//아래에 있는 모든 mapping은 문자열/api를 포함해야한다.
public class UserController {
private final TokenServiceImpl tokenService;
private final TokenProvider tokenProvider;
Cookie cookie = new Cookie("Cookie","forSecure");
@PostMapping(value = "/signin")
//ResponseEntity는 httpentity를 상속받는 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스이고, 응답으로 변환될 정보를 모두 담은 요소들을 객체로 사용 된다.
public ResponseEntity login(@RequestBody UserDTO userDTO, HttpServletResponse response){
try {
tokenService.loginMethod(userDTO, response);
cookie.setMaxAge(7*24*60*60);
cookie.setHttpOnly(true); //token 쿠키 저장 방식의 csrf 취약 문제 방지 위해 httponly true 설정
cookie.setSecure(true); //security : true
cookie.setPath("/");
response.addCookie(cookie);
return ResponseEntity.ok().body("SignIn Success");
} catch (Exception e) {
ResponseDTO responseDTO = ResponseDTO.builder().error(e.getMessage()).build();
return ResponseEntity
.badRequest()
.body(responseDTO);
}
}
@GetMapping(value = "/check")
public ResponseEntity checkUser(HttpServletRequest request) {
UserDTO result = tokenService.decodeJWT(request);
return ResponseEntity.ok().body(result);
}
@GetMapping("/token/refresh")
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
String refresh_token = request.getHeader(AUTHORIZATION);
if(refresh_token != null && refresh_token.startsWith("Bearer ")) {
try {
UserDTO user = tokenService.decodeJWT(request);
tokenService.refreshToken(user, response);
Map<String, String> message = new HashMap<>();
message.put("message","Refresh The Token");
new ObjectMapper().writeValue(response.getOutputStream(), message); //토큰 전송
}catch (Exception exception) {
if (exception.getMessage().startsWith("The Token's Signature")) { //조작된 토큰 일때
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", "Incorrect Token Do Re-Login");
}
else if (exception.getMessage().startsWith("The Token has expired")) {
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", "Token Has Expired Do Refresh");
}
else {
log.error("Error logging in: {}", exception.getMessage());
response.setHeader("error", "Unexpected Error...");
}
response.setStatus(FORBIDDEN.value()); //forbidden error code로 보낸다.
//response.sendError(FORBIDDEN.value());
Map<String, String> error = new HashMap<>();
error.put("error_message", "Make User Re-Login");
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
throw new RuntimeException("Access token is missing");
}
}
}
login :
- tokenservice의 loginMethod로 유저 정보를 보낸다.
- XSS방지 cookie설정
checkUser :
- token이 담긴 request를 tokenservice의 decodeJWT로 보낸다
- 클라이언트의 로그인 유지와 정보 교환에 사용된다
refreshToken :
- 받은 refresh-token이 존재하면 tokenService의 decodeJWT로 검사를 하고 유저 정보를 불러온다.
- 불러온 유저 정보로 tokenService의 refreshToken으로 토큰 생성 및 재발급을 진행한다.
- 오류 종류에 따른 예외 처리를 진행 한다.
@Service
@RequiredArgsConstructor
public class TokenServiceImpl implements TokenService{
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
@Value("${jwt.secret}")
private String SECRET_KEY;
@Override
public void loginMethod(UserDTO userDTO, HttpServletResponse response) {
String email = userDTO.getEmail();
UserEntity info = userRepository.findByEmail(email);
if(info == null){
throw new UsernameNotFoundException("User not found in the database");
}
else {
String password = info.getPassword();
boolean verify = passwordEncoder.matches(userDTO.getPassword(), password);
if(verify){
tokenProvider.createToken(info.toDTO(), response);
}
else {
throw new RuntimeException("WrongPassword");
}
}
}
@Override
public void refreshToken(UserDTO user, HttpServletResponse response) {
tokenProvider.createToken(user, response);
}
@Override
public UserDTO decodeJWT(HttpServletRequest request) {
String access_token = request.getHeader(AUTHORIZATION);
if(access_token != null && access_token.startsWith("Bearer ")) {
try {
String token = access_token.substring("Bearer ".length());
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY.getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String email = decodedJWT.getSubject();
String name = decodedJWT.getIssuer();
String role = decodedJWT.getClaim("role").toString().replace("\"", "");
UserDTO userDTO = UserDTO.builder()
.email(email)
.name(name)
.role(role)
.build();
return userDTO;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
else {
throw new RuntimeException("No Token");
}
}
}
loginMethod : login시 유저 확인과 유저일경우 token을 생성 하여 발급한다.
refreshToken : 다시 access,refresh token을 생성하여 재발급한다.
decodeJWT : token을 decode하여 유저정보를 return한다.
@Component
public class TokenProvider {
@Value("${jwt.secret}")//application.yml 파일에 secret-key 보관
private String SECRET_KEY;
public void createToken(UserDTO user, HttpServletResponse response) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY.getBytes()); //token 생성 알고리즘
String access_token = JWT.create() //access token 생성
.withSubject(user.getEmail())//이름을 유일한 유저 정보로 하여 토큰의 중복 방지
.withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 *1000)) //기간 설정 -> 지금으로 부터 + ???
.withIssuer(user.getName())
.withClaim("role", user.getRole())
.sign(algorithm);
String refresh_token = JWT.create() //refresh token 생성
.withSubject(user.getEmail())
.withExpiresAt(new Date(System.currentTimeMillis() + 300 * 60 *1000))
.withIssuer(user.getName())
.withClaim("role", user.getRole())
.sign(algorithm);
response.setHeader("access_token", access_token);//header에 담아서 보낸다
response.setHeader("refresh_token", refresh_token);
}
}
유저 정보로 토큰을 생성합니다.