JWT를 진행하기 앞서 Spring Security에 대해 알고 가야한다.
이전 블로그에서 작성했었는데 https://hmproject-1.tistory.com/48 이 블로그를 참고 하면 좋을것 같다.
**인증( Authentication ): 사용자 신원을 확인하는 프로세스 (로그인)
**인가 ( Authorization ): 특정 리소스에 접근하려는 회원의 권한을 확인하는 것 (인증 이후)
**권한 : 인증된 사용자가 어떤 일을 할 수 있는지 관리하는 것
인증,인가,권한이 무엇인지 정도는 알고 있어야 이해가 쉬울 것이다. (아직 나도 이해가 잘 되어 있는 상태는 아니다)
현재 내가 작성한 테이블
아래 정리한 내용은 여러 블로그나 자료를 보고 진행되는 과정을 적은것이다.
JWT 기반 인증 과정
1.로그인 요청 : 사용자가 서버에 아이디,비밀번호를 담은 로그인 요청을 보냄
2.정보 확인 및 토큰 생성 : 사용자의 정보가 맞다면 토큰을 생성
3.토큰 발급 : 생성한 토큰을 사용자에게 전달
4.토큰 저장 : 사용자는 받은 토큰을 저장함 (쿠키, 로컬 스토리지)
5.토큰 전송 : 사용자는 해당 웹사이트 모든 요청에 대해서 발급 받은 토큰을 포함해서 보냄 -> 주로 HTTP 요청의 Authorization 헤더에 JWT를 포함시켜 서버로 전송 (나는 여기서 쿠키에 담아 보냈다)
6.토큰 검증 : 서버는 매 요청 마다 토큰을 검증 -> 토큰변조, 유효기간 만료 (JWTFilter)
7.요청 처리 : 토큰이 유효하다면 요청처리 -> 사용자에게 결과 전달
-> 로그인 (인증) : 로그인 요청을 받은 후 토큰을 생성하여 응답
위 인증 과정을 코드로 작성했다
- 사용자가 입력한 아이디와 비밀번호(사용자정보)를 가지고 로그인(인증) 요청 (Request)
<form th:action="@{/signin}" th:object="${signIn}" method="POST">
<div class="group">
<label for="signInId" class="label">아이디</label>
<input id="signInId" type="text" th:field="*{userId}" class="input signInId">
</div>
<div class="group">
<label for="signInPwd" class="label">비밀번호</label>
<input id="signInPwd" type="password" th:field="*{userPwd}" class="input signInPwd">
</div>
<!--로그인 실패 메세지-->
<div th:if="${error}" class="alert">
<p th:text="${error}"></p>
</div>
<div class="group">
<input id="check" type="checkbox" class="check" checked>
<label for="check"><span class="icon" style="border: 0.3px solid #aaa;"></span>로그인 상태 유지</label>
</div>
<div class="group">
<button type="submit" class="button signInBnt" >로그인</button>
</div>
</form>
- Spring Security 코드
요청이 들어오면 AuthenticationFilter가 요청을 가로챈다
*** Spring Security가 설정되어 있는 경우, 모든 요청은 먼저 Spring Security의 필터 체인으로 전달한다.
요청이 인증이 필요한지 체크 -> 이 과정에서는 JWT가 생성되기 전 이므로 JWTFilter가 인증에 관여하지 않는다고 한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
System.out.println("스프링 시큐리티 상단");
// 모든 페이지에 대한 접근권한 설정, 사이트 위변조 방지 해제
http //authorizeHttpRequests 어떤 요청에 대해 어떤 권한이 필요한지 설정
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests //.requestMatchers : 특정 URL 지정
.requestMatchers("/", "/sms/send", "/signin", "/signup", "/idcheck", "/signform",
"/css/**", "/js/**", "/images/**", "/fonts/**", "/scss/**").permitAll() // 특정 경로에 대한 접근 권한 허용
.anyRequest().authenticated() // 그 외 모든 요청은 권한 필요
)
// 보안상의 이유로 다른 도메인에서 온 요청 차단
// CSRF 비활성화 -> 모든 도메인의 요청 허용
.csrf(AbstractHttpConfigurer::disable)
.logout((logout) -> logout.logoutSuccessUrl("/").permitAll())
// 세션 설정, JWT를 통한 인증, 인가 작업을 위해서는 세션을 무상태 (STATELESS) 로 설정하는 것이 중요!
// 스프링 시큐리티 -> 기본적으로 인증이 필요한 요청에 대해 세션을 생성하고 관리하는 기능을 제공
// JWT 인증 방식 -> 세션 생성 X
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// JWTFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
http.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
System.out.println("스프링 시큐리티 필터 부분 완료");
return http.build(); // HttpSecurity 객체를 빌드하는 마지막 호출
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authBuilder.userDetailsService(customUserDetailsService) // 사용자 정보 가져오기
.passwordEncoder(passwordEncoder()); // 비밀번호 암호화하고 검증
return authBuilder.build(); // AuthenticationManager 객체 생성
}
LoginFilter 코드
그리고 원래는 UsernamePasswordAuthenticationFilter를 상속받는 LoginFilter를 거쳐 사용자정보를 인증 해야하는데.. 하다보니 이 필터를 거치지 않고 처리를 하게 되었다.. 제대로 이해가 되지 않은 상태에서 마음이 조급해 막 하다보니 이런 사태가 일어난것.. 같다. 아직까지도 잘 모르겠다..
// 로그인 요청을 처리하는 필터
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
@Override //attemptAuthentication : 사용자의 아이디와 비밀번호를 확인하는 작업
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//사용자(클라이언트) 요청에서 로그인 id, password 추출
String userId = obtainUsername(request);
String userPassword = obtainPassword(request);
//스프링 시큐리티에서 userId와 userPassword를 검증하기 위해서는 token에 담아야 함 사용자 식별자(id,name), 비밀번호, 권한(roles)
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userId, userPassword, null);
System.out.println("사용자 정보 확인" + userId + userPassword);
// //token에 담은 검증을 위한 AuthenticationManager를 사용하여 인증(로그인) 시도
return authenticationManager.authenticate(authToken);
}
@Override // 인증(로그인) 성공시 실행(JWT토큰 생성)
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
// 인증된 사용자의 id 추출
String userId = authentication.getName();
// 권한(role) 추출
// 권한(role) 목록을 가져옴 GrantedAuthority: 사용자 권한을 나타내는 객체
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 권한 목록 순회 객체 : Iterator
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
String role = "ROLE_USER"; // 권한이 없을 경우 -> 기본 권한 설정(기본값)
if(iterator.hasNext()) { // 권한 목록에 권한이 있을 경우 true, 비어있으면 false
GrantedAuthority auth = iterator.next();
role = auth.getAuthority(); // 첫 번째 권한을 role에 저장
}
// JWT 토큰 생성
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("role", role); // role을 claims에 추가
//JWTUtil에 token 생성 요청
String token = jwtUtil.create(claims, LocalDateTime.now().plusHours(1));
System.out.println("생성된 JWT 토큰: " + token); // 로그로 생성된 토큰 확인
// Authorization : 서버에 인증 정보를 전달하기 위한 헤더.
// 생성한 토큰 앞에 타입인 "Bearer " 접두사를 붙여서 HTTP의 헤더에 추가 (Bearer : 인증방식)
response.addHeader("Authorization", "Bearer " + token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
// 실패 시 401 응답코드 보냄
response.setStatus(401);
}
}
SginController 코드
요청한 URL과 매핑된 후, 사용자가 입력한 아이디, 비밀번호를 authenticate메서드로 전송
// 첫 로그인 요청
@PostMapping("/signin")
public String signIn(@RequestParam(value="userId") String userId, @RequestParam(value="userPwd") String userPwd,
RedirectAttributes redirectAttributes, HttpServletResponse response) {
// 사용자 로그인 호출
String token = memberService.authenticate(userId, userPwd, response);
if (token != null) {
redirectAttributes.addFlashAttribute("success", "로그인에 성공하였습니다.");
return "redirect:/";
}
else {
// 인증 실패: 로그인 페이지로 다시 이동하고 에러 메시지 전달
redirectAttributes.addFlashAttribute("error", "아이디 또는 비밀번호가 틀립니다.");
return "redirect:/signform";
}
}
MemberService 코드
authenticate메서드 호출하여 -> customUserDetailsService를 사용해 사용자의 정보를 데이터베이스에서 조회하고, UserDetails객체로 반환 한다.
// 로그인
public String authenticate(String userId, String userPwd, HttpServletResponse response) {
// 사용자 정보 로드 및 권한 초기화
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);
String role = "ROLE_USER"; // 기본 권한 (기본값은 일반 사용자)
Long accommKey = null; // 숙소 관리자 키 초기화
// 사용자 정보가 존재하는 경우 (일반 유저, 사이트 관리자 또는 숙소 관리자)
if (userDetails != null) {
// 권한을 확인하여 부여
role = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER");
}
if ("ROLE_ACCOMMODATION_ADMIN".equals(role)) {
if (userDetails instanceof CustomUserDetails) {
accommKey = ((CustomUserDetails) userDetails).getMemberDTO().getAccommodationNo();
}
role = "ROLE_ACCOMMODATION_ADMIN";
}
// JWT 토큰 생성 및 쿠키 저장
if (userDetails != null && passwordEncoder.matches(userPwd, userDetails.getPassword())) {
// JWT에 포함할 클레임(정보) 생성
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("role", role);
// 숙소 관리자일 경우 숙소 키 추가
if ("ROLE_ACCOMMODATION_ADMIN".equals(role)) {
claims.put("accommKey", accommKey);
}
// 토큰생성
LocalDateTime expireAt = LocalDateTime.now().plusHours(1);
String token = jwtUtil.create(claims, expireAt);
// 토큰 쿠키에 저장
Cookie accessCookie = new Cookie("Authorization", token);
accessCookie.setMaxAge(60); // 1분 동안 유효
accessCookie.setPath("/"); // 모든 경로에 대해 쿠키 전송
accessCookie.setDomain("localhost"); // 도메인 설정
accessCookie.setSecure(false);
response.addCookie(accessCookie);
return token;
}
// 인증 실패 시 null 반환
return null;
}
CustomUserDetailsService 코드
사용자가 일반 유저나 사이트 관리자인 경우 loadUserByUsername메서드에서 정보를 조회하고,
숙소 관리자 라면 accommodationAdminByUserId에서 정보를 조회하여 CustomUserDetails로 반환 한다.
@Override
// 일반유저 & 사이트 관리자
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
// 일반 사용자 및 사이트 관리자 정보
MemberDTO member = memberMapper.findByUserId(userId);
// 일반 유저가 아닌 경우, 숙소 관리자 정보 로드
if (member == null) {
member = memberMapper.accommodationAdminByUserId(userId);
if (member == null){
// 숙소 관리자도 아닐 경우 예외 발생
throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + userId);
}
}
return new CustomUserDetails(member);
}
CustomUserDetails 코드
CustomUserDetails는 UserDetails 인터페이스를 구현한 객체로, 스프링 시큐리티가 이해할 수 있는 형식으로 사용자 정보와 권한을 포함한다.
CustomUserDetails객체가 UserDetails로 반환되며, MemberService의 authenticate메서드로 전달
// 사용자 정보를 스프링 시큐리티가 이해할 수 있는 형식으로 제공 하는 역할
// (사용자 인증, 사용자 권한 관리)
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final MemberDTO memberDTO;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(memberDTO.getRole()));
}
@Override
public String getPassword() {
return memberDTO.getUserPwd();
}
@Override
public String getUsername() {
return memberDTO.getUserId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// MemberDTO 객체를 반환하는 메서드
public MemberDTO getMemberDTO() {
return memberDTO;
}
}
다시 아래의 MemebrService코드로 돌아와 보면 UserDetails객체에서 사용자 정보를 조회한 데이터를 기분으로 사용자의 권한을 확인하고, role변수에 저장, 권한이 숙소 관리자 라면 추가로 업소키를 추가로 설정해 준다
// 사용자 정보 로드 및 권한 초기화
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);
String role = "ROLE_USER"; // 기본 권한 (기본값은 일반 사용자)
Long accommKey = null; // 숙소 관리자 키 초기화
// 사용자 정보가 존재하는 경우 (일반 유저, 사이트 관리자 또는 숙소 관리자)
if (userDetails != null) {
// 권한을 확인하여 부여
role = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER");
}
if ("ROLE_ACCOMMODATION_ADMIN".equals(role)) {
if (userDetails instanceof CustomUserDetails) {
accommKey = ((CustomUserDetails) userDetails).getMemberDTO().getAccommodationNo();
}
role = "ROLE_ACCOMMODATION_ADMIN";
}
여기서 JWT토큰을 생성하는 로직을 추가하고, 생성된 토큰을 쿠키에 저장해 SignController에 반환해 준다.
// JWT 토큰 생성 및 쿠키 저장
if (userDetails != null && passwordEncoder.matches(userPwd, userDetails.getPassword())) {
// JWT에 포함할 클레임(정보) 생성
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("role", role);
// 숙소 관리자일 경우 숙소 키 추가
if ("ROLE_ACCOMMODATION_ADMIN".equals(role)) {
claims.put("accommKey", accommKey);
}
// 토큰생성
LocalDateTime expireAt = LocalDateTime.now().plusHours(1);
String token = jwtUtil.create(claims, expireAt);
// 토큰 쿠키에 저장
Cookie accessCookie = new Cookie("Authorization", token);
accessCookie.setMaxAge(60); // 1분 동안 유효
accessCookie.setPath("/"); // 모든 경로에 대해 쿠키 전송
accessCookie.setDomain("localhost"); // 도메인 설정
accessCookie.setSecure(false);
response.addCookie(accessCookie);
return token;
}
이제 위 과정에서 생성된 JWT 토큰은 Controller로 반환되어
HttpServletResponse 객체를 통해 쿠키에 저장되어 클라이언트 측에 전달이되고,
이후 요청에 포함에되 다시 서버로 전송된다. 여기까지가 인증과정이다.
아래의 코드는 실질적으로 JWT 토큰을 생성하기 위한 코드이다.
JWTUtil코드
JWT (JSON Web Token) 를 실제로 생성하고 검증하는 클래스 이다.
시크릿 키 생성 (터미널에 따로 명령어를 적어 생성)
openssl rand -hex 32 : jwt 키 생성
application.properties 설정
jwt.secret = 생성한 비밀키
#1일
jwt.expiration = 86400000
비밀키 객체를 생성 -> 현재 설정파일에 저장된 키는 문자열로 저장되어 있으므로 바이트 배열로 변환 -> HMAC-SHA 알고리즘에 사용할 수 있는 적합한 형태로 바꿔준다. -> JWT의 서명을 생성하고 검증할 때 사용(실제 암호화키 역할)
// 설정 파일에 저장된 비밀키 값
// -> secretKey -> JWT 토큰을 생성할 때 서명( Signature )을 만들기 위해 사용
@Value("${jwt.secret}")
private String secretKey;
// 설정 파일에 저장된 유효시간 ->
// expirationTime -JWT 토큰 생성시 사용하여 만료 시간 설정
@Value("${jwt.expiration}")
private long expirationTime
// 비밀키 객체 생성
private Key getKey() {
// base64 인코딩을 하지않고, 키를 직접 바이트 배열로 변환
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
create 메서드 : JWT토큰 생성
isExpired 메서드 : 토큰 만료 여부
getUserIdFromToken 메서드 : 사용자 아이디 추출
getRoleFromToken 메서드 : 사용자 권한 추출
validate 메서드 : JWT 유효성 검사
@Slf4j
@Component
// JWT의 생성 및 검증 처리
public class JWTUtil {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expirationTime;
// 비밀키 객체 생성
private Key getKey() {
// base64 인코딩을 하지않고, 키를 직접 바이트 배열로 변환
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
// JWT 생성 메서드
public String create(Map<String, Object> claims, LocalDateTime expireAt) {
// 임의로 만든 암호키로 key 설정
var key = getKey();
// token의 Expire(만기) 시간을 객체로 변환
var _expireAt = Date.from(expireAt.atZone(ZoneId.systemDefault()).toInstant());
// JWT 생성 및 로그 출력
String token = Jwts.builder()
.setClaims(claims) // 사용자 정보 설정
.setExpiration(_expireAt) //토큰의 만료 시간을 설정
.signWith(key, SignatureAlgorithm.HS256) // 서명 방식과 키 설정
.compact(); //JWT를 문자열로 반환
log.info("토큰생성 : {}", token);
return token;
}
// 토큰의 만료 여부를 확인하는 메서드
public Boolean isExpired(String token) {
try {
// 서명 검증 및 토큰 파싱
Claims claims = Jwts.parser()
.setSigningKey(getKey()) // 서명 검증에 사용할 키 설정
.build()
.parseClaimsJws(token) // 토큰 파싱
.getBody(); // 클레임 반환
// 만료 시간과 현재 시간 비교
return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
// 토큰이 만료된 경우 true 반환
return true;
} catch (Exception e) {
// 기타 예외 발생 시 false 반환
return false;
}
}
// JWT에서 사용자 ID를 추출
public String getUserIdFromToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(getKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("userId", String.class);
} catch (Exception e) {
log.error("JWT에서 사용자 ID를 추출하는 중 오류 발생: " + e.getMessage());
return null;
}
}
// JWT에서 role을 추출
public String getRoleFromToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(getKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("role", String.class);
} catch (Exception e) {
log.error("JWT에서 권한을 추출하는 중 오류 발생: " + e.getMessage());
return null;
}
}
// JWT 유효성 검사
public void validate(String token) {
var key = getKey();
try {
var result = Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token); // 토큰 파싱
result.getPayload().forEach((key1, value1) -> log.info("key : {}, value : {}", key1, value1));
} catch (Exception e) {
if (e instanceof SignatureException) {
throw new RuntimeException("JWT Token Invalid Exception");
} else if (e instanceof ExpiredJwtException) {
throw new RuntimeException("JWT Token Expired Exception");
} else {
throw new RuntimeException("JWT Exception");
}
}
}
}
인증과정이 지나고 난 후 추가적인 요청을 보낼 때, 위에 말한것 처럼 모든 요청은 먼저 Spring Security의 필터 체인으로 전달되기 되고 이 때는 JWT토큰이 생성된 후 이므로 JWTFilter가 요청을 거쳐 인가 과정이 진행된다.
JWT 인가 과정
1.토큰 검증 : 위의 4~6까지 동일
2.권한 확인 : 토큰이 유효할 때, 사용자 정보를 확인 -> 요청 처리 권한 있는지 확인
3.요청 처리/거부 : 권한이 올바르다면 요청을 처리, 올바르지 않다면 거부
-> 경로 접근 (인가) : JWT Filter를 통해서 요청의 쿠키 에서 JWT를 찾아 검증하고 일시적 요청에 대한 Session 생성 => 생성된 Session은 요청이 종료할 시 소멸됨
JWTFilter코드
요청의 쿠키에서 JWT를 찾아 검증하고, 그에 따라 사용자에게 적절한 권한을 부여
사용자 요청 -> 브라우저에 저장된 JWT 토큰이 자동으로 쿠키에 포함 -> 이 토큰으로 요청을 보냄
// 모든 요청에 대해 JWT의 유효성을 검사하는 필터
@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 쿠키에서 JWT 토큰을 찾음
Cookie[] cookies = request.getCookies();
String token = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("Authorization".equals(cookie.getName())) {
token = cookie.getValue();
log.info("JWTFilter: 쿠키에서 토큰 찾음 {}", token);
break;
}
}
}
// JWT 토큰이 없거나 유효하지 않은 경우
if (token == null) {
log.warn("JWTFilter: JWT 토큰이 null입니다.");
filterChain.doFilter(request, response);
return;
}
if (jwtUtil.isExpired(token)) {
log.warn("JWTFilter: JWT 토큰이 만료되었습니다.");
filterChain.doFilter(request, response);
return;
}
// 유효한 토큰인 경우 사용자 정보 추출
String loginId = jwtUtil.getUserIdFromToken(token); // 사용자 ID 추출
String role = jwtUtil.getRoleFromToken(token); // 사용자 권한 추출
// DTO 생성 및 설정
MemberDTO memberDTO = new MemberDTO();
memberDTO.setUserId(loginId);
memberDTO.setRole(role);
// CustomUserDetails에 사용자 정보 설정
CustomUserDetails customUserDetails = new CustomUserDetails(memberDTO);
log.info("JWTFilter: JWT 토큰 유효! 사용자 이름: {}", customUserDetails.getUsername());
log.info("JWTFilter: JWT 토큰 유효! 사용자 권한: {}", customUserDetails.getAuthorities());
// 스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(
customUserDetails, // 사용자 정보
null, // 비밀번호는 null로 설정
customUserDetails.getAuthorities() // 권한 설정
);
// SecurityContext에 인증 정보 설정
SecurityContextHolder.getContext().setAuthentication(authToken);
System.out.println("JWTFilter: 인증 정보가 SecurityContext에 설정되었습니다.");
// 유효한 토큰인 경우, 이후 처리 진행
filterChain.doFilter(request, response);
}
}
위 코드에서 JWT를 검증하고 사용자의 권한을 확인하여 인증 정보를 설정
지금까지 로그인을 구현한 코드이다..
아직 스프링 시큐리티에 적용을 제대로 못한 상황이고 이제 로그아웃, 리프레쉬토큰을 활용해 로그인유지.. 등 구현을 해야하는 상황이다