이전글 : https://kora1492.tistory.com/83
JWT
JWT(Jason Web Token)는 웹 표준으로 JSON 형식의 데이터를 사용하여 정보를 안전하게 전송하는 방식을 정의
Header : JWT가 어떤 알고리즘으로 서명되었는지에 대한 정보를 포함.
alg : 암호화 알고리즘은 어떤 건지
typ : 데이터, JWT 를 쓸거다.
{
"alg": "HS256",
"typ": "JWT"
}
Payload : 실제 전송할 데이터인 Claim(요구사항) 정보가 들어가 있다.
Signature : Header와 Payload에 대한 서명값이 포함, 서명값은 Header에서 지정한 알고리즘과 시크릿키를 이용하여 생성
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
* JWT의 "Claims"는 크게 세 가지 유형
1. Registered Claims
2. Public Claims
3. Private Claims
JWT에서 정의된 기본적인 Claim 구조
iss (Issuer) : JWT를 발급한 개체(보통 URL 또는 이메일 주소)
sub (Subject) : JWT가 제공하는 서비스의 주체(사용자 ID 등)
aud (Audience) : JWT를 받아들일 대상(보통 URL 또는 이메일 주소)
exp (Expiration Time) : JWT의 만료 시간
nbf (Not Before) : JWT의 유효 시작 시간
iat (Issued At) : JWT의 발급 시간
jti (JWT ID) : JWT의 고유 ID
또한 사용자가 직접 정의할 수 있는 Claim을 추가 가능
JWT를 생성할 때는 JwtBuilder를 사용하여 Claim 정보를 설정하고, setClaims() 메소드를 통해 Claim을 설정.
Map<String, Object> claims = new HashMap<>();
claims.put("name", "John Doe");
claims.put("email", "john.doe@example.com");
JwtBuilder builder = Jwts.builder().setClaims(claims).signWith(signKey, SignatureAlgorithm.HS256);
Claims claims = Jwts.parserBuilder().setSigningKey(signKey).build().parseClaimsJws(token).getBody();
String name = claims.get("name", String.class);
JWT를 통한 로그인 추가
- 유튜브 강의(https://www.youtube.com/watch?v=htYYSszfzv0&t=5431s)에선 리프레시 토큰은 별도로 설정하지 않는 Token 발행하여 쿠키에 저장하여 사용함.
- jwt 리프래쉬나 axios 인터셉트를 통하여 리프래쉬 토큰 관리를 하면서 acess token 관리 유지가 되는 플로우가 있어야 한다는 의견이 있음.
build.gradle 의존성 추가 (jjwt)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Service
Jwt 서비스를 추가한다.
secretkey 의 경우, 길이가 부족하면 signing key 에 충분하지 않다는 오류가 발생되므로 key 값은 충분히 길게 작성할 것.
The signing key's size is 250미만 bits which is not secure enough for the HS256 algorithm
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class JwtServiceimpl implements JwtService {
private final String secretKey = "dfadfksfjadfaflsdfnklbnlknklfdfdlff123213123";
@Override
public String getToken(String key, Object value) {
Date expireTime = new Date();
expireTime.setTime(expireTime.getTime() * 1000 * 60 * 5);
byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secretKey);
Key signKey = new SecretKeySpec(secretByteKey, SignatureAlgorithm.HS256.getJcaName());
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("typ", "JWT");
headerMap.put("alg", "HS256");
Map<String, Object> map = new HashMap<>();
map.put(key, value);
JwtBuilder builder = Jwts.builder().setHeader(headerMap)
.setClaims(map)
.setExpiration(expireTime)
.signWith(signKey, SignatureAlgorithm.HS256);
return builder.compact();
}
@Override
public Claims getClaims(String token) {
if (token != null && !"".equals(token)) {
try {
byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secretKey);
Key signKey = new SecretKeySpec(secretByteKey, SignatureAlgorithm.HS256.getJcaName());
return Jwts.parserBuilder().setSigningKey(signKey).build().parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
// 만료됨
log.error("토큰 만료");
} catch (JwtException e) {
// 유효하지 않음
log.error("토큰 유효하지 않음");
}
}
return null;
}
}
- getToken () : 주어진 key-value 쌍을 포함하는 JWT를 생성
- 만료 시간 : 현재 시간으로부터 5분으로 설정
secretKey를 byte 배열로 변환하여 SecretKeySpec 객체를 생성
JWT의 헤더와 Claims를 설정
signWith 메소드를 사용하여 서명 알고리즘과 함께 JWT를 서명, 생성된 JWT를 반환합니다. - getClaims() : 주어진 JWT에서 Claims를 추출
- secretKey를 byte 배열로 변환하여 SecretKeySpec 객체를 생성
Jwts.parserBuilder().setSigningKey(signKey).build().parseClaimsJws(token).getBody() 를 사용하여 JWT에서 추출된 Claims를 반환
LoginController
import com.example.study_springbootvue.entity.Member;
import com.example.study_springbootvue.repository.MemberRepository;
import com.example.study_springbootvue.service.JwtService;
import com.example.study_springbootvue.service.JwtServiceimpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Slf4j
@RestController
public class LoginController {
@Autowired
MemberRepository memberRepository;
@Autowired
JwtService jwtService;
@PostMapping("/api/member/login")
public ResponseEntity login(@RequestBody Map<String, String> params
, HttpServletResponse response
){
Optional<Member> optionalMember = memberRepository.findByEmailAndAndPassword(params.get("email"), params.get("password"));
Member member;
if(optionalMember.isPresent()){
member = optionalMember.get();
log.info("멤버 로그인");
int id = member.getId();
String token = jwtService.getToken("id", id);
Cookie cookie = new Cookie("token", token);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
return new ResponseEntity<>(id, HttpStatus.OK);
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
@PostMapping("/api/member/logout")
public ResponseEntity logout(HttpServletResponse res) {
Cookie cookie = new Cookie("token", null);
cookie.setPath("/");
cookie.setMaxAge(0);
res.addCookie(cookie);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/api/member/check")
public ResponseEntity check(@CookieValue(value = "token", required = false) String token){
Claims claims = jwtService.getClaims(token);
if (claims != null) {
log.info(claims+" : claims is not null");
int id = Integer.parseInt(claims.get("id").toString());
return new ResponseEntity<>(id, HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.OK);
}
}
App.Vue
<script>
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import store from "@/scripts/store";
import axios from "axios";
import {watch} from "vue";
import {useRoute} from "vue-router";
export default {
name: 'App',
components: {
Footer,
Header
},
setup() {
const check = () => {
axios.get("/api/member/check").then(({data}) => {
console.log(data);
store.commit("setAccount", data || 0);
})
};
const route = useRoute();
watch(route, () => {
check();
})
}
}
</script>
- check 함수는 axios 라이브러리를 사용하여 /api/member/check 경로로 GET 요청을 보내고, 이에 대한 응답으로 받은 데이터를 setAccount 뮤테이션을 호출하여 Vuex 상태 관리를 통해 저장
- route 객체는 useRoute() 훅을 사용하여 현재 활성화된 라우트 정보를 얻어옵니다. 그리고 watch() 함수를 사용하여 route 객체가 변경될 때마다 check 함수를 호출하여 로그인한 사용자의 정보를 업데이트
'프로그래밍' 카테고리의 다른 글
[SpringBoot+Vue.js] 정리 (5) 기본 기능 완료 (0) | 2023.04.11 |
---|---|
[Spring MVC] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(1) (0) | 2023.04.09 |
[SpringBoot+Vue.js] 정리 (3) Vue router 추가 및 로그인 (0) | 2023.04.06 |
Aui Gird -> Toast UI Grid 로 전환하기(기록, 정리) (0) | 2023.04.05 |
[SpringBoot] 한국수출입은행 환율 API 가져오기 (0) | 2023.03.29 |