[작성일: 2023. 09. 22]
jwt를 위한 강제 로그인 진행
username과 password를 받는 것을 확인하기 위해 JwtAuthenticationFilter 클래스에서 코드를 수정한다.
// 로그인 요청을 하면 로그인 시도를 위해 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : 로그인 시도 중");
// 1. username, password 받아서
try {
BufferedReader br = request.getReader();
String input = null;
while ((input = br.readLine()) != null) {
System.out.println(input);
}
} catch (Exception e) {
e.printStackTrace();
}
// 코드 생략 ...
}
Postman으로 로그인 시도 시 콘솔창에 request에 담긴 username과 password가 찍히는 것을 볼 수 있다.
지금은 x-www-form-urlencoded로 데이터를 보냈는데 row-JSON으로 보내면 어떻게 될까?
json으로도 잘 담기는 것을 확인했다. 이제 내가 로그인 시도를 어떻게 하느냐에 따라 다른데, 최근에는 json으로 요청하는 경우가 많다. 그래서 간단하게 다음과 같이 파싱할 수 있다.
// 로그인 요청을 하면 로그인 시도를 위해 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : 로그인 시도 중");
// 1. username, password 받아서
try {
ObjectMapper om = new ObjectMapper();
User user = om.readValue(request.getInputStream(), User.class);
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}
// ... 코드 생략
}
로그인 시도를 하려면 토큰을 직접 하나 만들어야 한다. 원래는 자동으로 만들어주지만 지금은 없기 때문에 토큰을 직접 만들어준다.
이 때 AuthenticationManager에 토큰을 넣어서 던져주면 인증이 된다. 인증이 되면 authentication을 받을 수 있고, 여기에는 내 로그인 정보가 담기게 된다.
// 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter
// /login 요청 시 username, password POST로 전송하면 필터가 동작됨.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// 로그인 요청을 하면 로그인 시도를 위해 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : 로그인 시도 중");
// 1. username, password 받아서
try {
ObjectMapper om = new ObjectMapper();
User user = om.readValue(request.getInputStream(), User.class);
System.out.println(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 이게 실행될 때 PrincipalDetailsService에 loadUserByUsername()이 실행된다.
Authentication authentication = authenticationManager.authenticate(authenticationToken);
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println(principalDetails.getUser().getUsername());
// authentication 객체가 session 영역에 저장됨. => 로그인이 되었다는 의미
return authentication;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
authentication 객체가 session 영역에 저장을 해야하고 그 방법은 return을 해주면 된다.
리턴의 이유는 권한 관리를 security가 대신 해주기 때문에 편하려고 하는 것이다.
굳이 JWT 토큰을 사용하면서 세션을 만들 이유가 없다. 단지 권한 처리때문에 session을 넣어준다.
마지막으로 attempAuthentication 실행 후 인증이 정상적으로 처리되었으면 successfulAuthentication 함수를 실행할 수 있도록 코드를 작성한다. JWT 토큰을 만들어서 reuqest 요청 한 사용자에게 JWT 토큰을 response 해주면 된다.
// ... 코드생략
return null;
}
// attempAuthentication 실행 후 인증이 정상적으로 되면 이 함수가 실행된다.
// jWT 토큰을 만들어서 request를 요청한 사용자에게 jWT 토큰을 response 해주면 됨.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication 실행됨, 인증이 완료되었다는 뜻" );
super.successfulAuthentication(request, response, chain, authResult);
}
}
JWT 토큰 만들어서 응답하기
// attempAuthentication 실행 후 인증이 정상적으로 되면 이 함수가 실행된다.
// jWT 토큰을 만들어서 request를 요청한 사용자에게 jWT 토큰을 response 해주면 됨.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication 실행됨, 인증이 완료되었다는 뜻" );
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
String jwtToken = JWT.create()
.withSubject("token") // 임시
.withExpiresAt(new Date(System.currentTimeMillis() + (60000 * 10)))
.withClaim("id", principalDetails.getUser().getId())
.withClaim("username", principalDetails.getUser().getUsername())
.sign(Algorithm.HMAC512("token")); // 임시
response.addHeader("Authorization", "Bearer " + jwtToken);
}
현재시간 + (600000 * 10) 이 토큰의 만료시간이 된다. 이 만료시간 후에는 토큰을 다시 만들어주어야 하며 짧은 게 좋다.
jwToken을 생성해서 response Header에 담아주고 login을 요청해보면 Header에 Authorization과 token이 있는 것을 확인할 수 있다.
유저네임, 패스워드 로그인이 정상이면 JWT 토큰을 생성하고 클라이언트 쪽으로 JWT 토큰을 응답해준다.
요청할 때마다 JWT 토큰을 가지고 요청하며 서버는 JWT 토큰이 유효한지 판단해야 하는데 이것을 위해 필터를 하나 만들어야 한다.
JWT 토큰
시큐리티는 filter를 가지고 있는데 그 필터 중에 BisicAuthenticationFilter 라는 필터가 있다.
권한이나 인증이 필요한 특정 주소를 요청했을 때 BisicAuthenticationFilter를 무조건 타게 되어있다.
권한이나 인증이 필요한 주소가 아니라면 BisicAuthenticationFilter를 타지 않는다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
// 인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 타게 됨.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("인증이나 권한이 필요한 주소 요청 됨.");
String jwtHeader = request.getHeader("Authorization");
System.out.println(jwtHeader);
// JWT 토큰 검증해서 정상적인 사용자인지 확인
if ((jwtHeader == null) || !(jwtHeader.startsWith("Bearer"))) {
chain.doFilter(request, response);
return;
}
String jwtToken = request.getHeader("Authorization").replace("Bearer ", "");
String username = JWT.require(Algorithm.HMAC512("token")).
build().verify(jwtToken).getClaim("username").asString();
// 서명이 정상적으로 됨.
if (username != null) {
User user = userRepository.findByUsername(username);
PrincipalDetails principalDetails = new PrincipalDetails(user);
// Jwt 토큰 서명을 통해서 서명이 정상이면 authentication 객체를 만들어준다.
Authentication authentication =
new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
// 강제로 시큐리티 세션에 접근하여 Authentication 객체를 저장함.
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
}
다시 Postman에서 테스트를 해보면 이제는 인증이 되었기 때문에 400오류가 아닌 500오류가 뜬다.
🐣 출처: 인프런 최주호님 강의
이 글은 인프런의 최주호님 SpringBoot Security & JWT 강의를 보고 작성한 글입니다.
강의를 들으면서 정리한 글이므로 틀린 내용이나 오타가 있을 수 있습니다.