intellij JWT랑 Spring Security를 이용한 로그인, logout 경로로 오면 jwt쿠키 삭제 방법, login한 사용자가 login 페이지를 가려하는걸 막는 방법
JWT랑 Spring Security를 이용한 로그인
* 로그인
→ JWT 생성(db와 비교해 맞으면 JwtUtil의 createToken으로 생성)
→ 클라이언트 저장(response.addCookie(cookie))
→ 보호된 경로 설정 및 예외처리(SecurityConfig에 지정된 경로, 예외 발생시 /login 경로로 이동)
→ 클라이언트가 보호된 경로로 요청(jwtUtil의 extractToken으로 검증 및 Authentication 등록)
→ get요청을 제외한 다른 비동기 요청은 status 프론트에 보냄(JwtFilter)
→ JwtFilter에서 검증이 완료되면, 스프링 시큐리티가 알아서 관리(SecurityConfig) *
로그인
script문에서 입력한 username, password를 fetch문을 통해 body에 담아서 보낸다.
에러 발생시 에러 내용 및 상태코드를 알려주고, 성공시 "/" 경로로 보낸다.
<form>
<input name="username" id="username">
<input name="password" type="password" id="password">
</form>
<button onclick="loginJWT()">전송</button>
<script>
function loginJWT(){
fetch('/login/jwt', {
method : 'POST',
headers : {'Content-Type': 'application/json'},
body : JSON.stringify({
username : document.querySelector('#username').value,
password : document.querySelector('#password').value
})
})
.then(response => {
if (!response.ok) {
throw new Error('로그인 실패: 상태 코드 ' + response.status);
}
return response.text();
})
.then(token => {
console.log('JWT:', token);
window.location.href = "/";
})
.catch(error => {
alert(error.message);
});
}
</script>
JWT 생성(db와 비교해 맞으면 JwtUtil의 createToken으로 생성)
body의 내용을 Map형식으로 받아온다.
public String loginJWT(@RequestBody Map<String, String> data, HttpServletResponse response){
받아온 username, password로 Authentication 객체를 생성한다.
var authToken = new UsernamePasswordAuthenticationToken(data.get("username"), data.get("password"));
위에서 만든 Authentication 객체를 db와 비교하여 성공/실패 판단한다.
성공하면 auth에 인증 완료된 객체가 들어온다.
var auth = authenticationManagerBuilder.getObject().authenticate(authToken);
전역 보안 컨텍스트(SecurityContextHolder)에 인증 정보를 저장
SecurityContextHolder.getContext().setAuthentication(auth);
전역 보안 컨텍스트(SecurityContextHolder)에 저장된 인증 정보를 JwtUtil.createToken메서드에 전달해서 JWT를 생성
var jwt = JwtUtil.createToken(SecurityContextHolder.getContext().getAuthentication());
*JwtUtil의 createToken 메서드 내용 ↓*
public static String createToken(Authentication auth) {
CustomUser user = (CustomUser) auth.getPrincipal();
var authorities = auth.getAuthorities().stream().map(a -> a.getAuthority())
.collect(Collectors.joining(","));
String jwt = Jwts.builder()
.claim("id", user.id)
.claim("username", user.getUsername())
.claim("displayName", user.displayName)
.claim("authorities", authorities)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + 100000)) //유효기간 100초
.signWith(key)
.compact();
return jwt;
}
클라이언트 저장(response.addCookie(cookie))
위에서 생성된 JWT를 "jwt" 라는 이름의 cookie 객체를 생성
var cookie = new Cookie("jwt", jwt);
JWT만들었을때의 유지 기간이랑 동일하게 기간 설정 (100초)
cookie.setMaxAge(100);
쿠키를 자바스크립트로 조작 못하게 (XSS 공격 방지)
cookie.setHttpOnly(true);
쿠키가 사이트 전체 경로에 대해 유효하도록 지정 ( /로 시작하는 모든 경로에 지정)
cookie.setPath("/");
클라이언트(브라우저)에 저장
response.addCookie(cookie);
보호된 경로 설정 및 예외처리(SecurityConfig에 지정된 경로 , 예외 발생시 /login 경로로 이동)
authorize를 받아, 어떤 경로에 어떤 권한을 줄지 설정
http.authorizeHttpRequests((authorize) -> authorize
/login, /css/**, /js/** 경로는 누구나 접근 허용
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
/detail/, /delete/, /edit/로 시작하는 모든 요청은 로그인(인증)이 된 사용자만 접근 허용
.requestMatchers("/detail/**", "/delete/**", "/edit/**").authenticated()
위에서 명시하지 않은 모든 나머지 요청은 접근 허용
.anyRequest().permitAll()
인증이나 권한이 없는 상태에서 발생하는 예외를 처리할 방법을 지정
http.exceptionHandling(exception ->
인증이 없는 사용자가 인증이 필요한 URL에 접근할 경우 자동으로 /login 페이지로 리다이렉트
exception.authenticationEntryPoint((request, response, authException) -> {
response.sendRedirect("/login");
})
클라이언트가 보호된 경로로 요청(jwtUtil의 extractToken으로 검증 및 Authentication 등록)
SpringConfig에서 보호된 경로를 인식하고 JwtFilter로 이동
cookie정보를 받아와 쿠키값이 null인지, "jwt" 이름이 맞는지 확인
Cookie[] cookies = request.getCookies();
String jwtCookie = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("jwt".equals(cookie.getName())) {
jwtCookie = cookie.getValue();
break;
}
}
}
JWT 토큰을 JwtUtil의 extractToken메서드로 검증하고, 토큰에 담긴 클레임(정보들)을 꺼냄
Claims claim = JwtUtil.extractToken(jwtCookie);
사용자 권한(authorities) 정보를 SimpleGrantedAuthority 객체 리스트로 변환
var arr = claim.get("authorities").toString().split(",");
var authorities = Arrays.stream(arr)
.map(SimpleGrantedAuthority::new)
.toList();
"id" 값을 꺼내서 Long 타입으로 변환
Long id;
try {
id = Long.valueOf(claim.get("id").toString());
} catch (NumberFormatException e) {
id = (long) Double.parseDouble(claim.get("id").toString());
}
JWT에서 꺼낸 사용자 정보를 기반으로 CustomUser 객체를 생성(password는 임시로 저장)
var customUser = new CustomUser(
id,
claim.get("username").toString(),
"none",
authorities
);
displayName 값을 꺼내서 customUser 객체의 displayName 필드에 저장
customUser.displayName = claim.get("displayName").toString();
매개변수를 이용해 인증 토큰 객체 생성
var authToken = new UsernamePasswordAuthenticationToken(customUser, "", authorities);
인증 토큰 객체에 요청(request)의 상세 정보를 붙이는 단계
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
최종 정보를 Authenticatioin에 저장(등록)
SecurityContextHolder.getContext().setAuthentication(authToken);
get요청을 제외한 다른 비동기 요청은 status 프론트에 보냄(JwtFilter)
비동기 요청은 HTTP 상태 코드를 반환하지만,
SecurityConfig에서 설정한 redirect는 브라우저가 자동으로 처리하지 않아서 클라이언트가 직접 대응
따라서 JwtFilter에서 조건문을 통해 /delete 경로(비동기)라면 프론트에게 상태코드를 보낸다.
프론트에서 script문으로 상태코드에 따라 /login페이지로 보낼지 설정
if (request.getRequestURI().startsWith("/delete")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
.then(res => {
if (res.status === 401) {
window.location.href = '/login';
throw new Error('Unauthorized');
}
return res.text();
})
JwtFilter에서 검증이 완료되면, 스프링 시큐리티가 알아서 관리(SecurityConfig)
해당 경로를 토대로 스프링 시큐리티가 알아서 관리해준다.
// 지정된 경로 보호
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
.requestMatchers("/detail/**", "/delete/**", "/edit/**").authenticated()
.anyRequest().permitAll()
);
// auth 없이 지정된 경로 접속 시 /login으로 이동
http.exceptionHandling(exception ->
exception.authenticationEntryPoint((request, response, authException) -> {
response.sendRedirect("/login");
})
);
logout 경로로 오면 jwt쿠키 삭제 방법
SecurityConfig - SecurityFilterChain -
http.logout(logout -> logout
.logoutUrl("/logout") // 기본 로그아웃 URL
.logoutSuccessHandler((request, response, authentication) -> {
// ✅ JWT 쿠키 삭제
Cookie cookie = new Cookie("jwt", null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
// ✅ 로그아웃 후 리디렉션
response.sendRedirect("/login");
})
);
작성
login한 사용자가 login 페이지를 가려하는걸 막는 방법
login controller에서 쿠키값을 비교해서 막기
@GetMapping("/login")
public String login(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("jwt".equals(cookie.getName())) {
// 토큰이 있으면 /로 리다이렉트
return "redirect:/";
}
}
}
// 토큰 없으면 로그인 페이지 뷰 반환
return "login.html";
}