JWT
[최종] JWT 구현하기 : 생성하고 인증하기
JANNNNNN
2024. 7. 13. 18:07
JwtAuthorizationFilter extends BasicAuthenticationFilter 생성
// 시큐리티가 filter를 가지고 있는데 그 필터중에 BasicAuthenticationFilter라는 것이 있음
// 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어 있음
// 만약에 권한이나 인증이 필요한 주소가 아니라면 이 필터를 사용하지 않음
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
// 인증이나 권한이 필요한 주소 요청이 있을 때 해당 필터를 타게 됨
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
super.doFilterInternal(request, response, chain);
System.out.println("인증이나 권한이 필요한 주소 요청 됨");
String jwtHeader = request.getHeader("Authorization");
System.out.println("jwtHeader = " + jwtHeader);
}
}
SecurityConfig 수정
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.addFilterBefore(new MyFilter3(), SecurityContextPersistenceFilter.class);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //세션 없는 상태로 만들겠다.
.and()
.addFilter(corsFilter) //@CrossOrigin는 인증이 없을때만 사용 가능함!, 인증이 필요할 땐 필터에 등록해야한다
.formLogin().disable()
.httpBasic().disable()
.addFilter(new JwtAuthenticationFilter(authenticationManager())) //AuthenticationManager를 파라미터로 꼭 넣어줘야함!!
//아래 FIlter가 추가됨
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userRepository)) //AuthenticationManager를 파라미터로 꼭 넣어줘야함!!
.authorizeRequests()
.antMatchers("/api/v1/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/manager/**")
.access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/admin/**")
.access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
}
이제 포스트맨으로 login을 시도하고, 권한이 필요한 페이지인 '/api/v1/user/'로 접근해봅시다!
DB에 저장되어 있는 username과 password가 일치한다면, authorization에 토큰값이 넘어왔을거에요.
그러면 이 권한을 이용해 페이지를 요청해보겠습니다.
아직은 403 에러가 발생한다!
JwtAuthorizationFilter 수정
//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 {
// super.doFilterInternal(request, response, chain);
System.out.println("인증이나 권한이 필요한 주소 요청 됨");
String jwtHeader = request.getHeader("Authorization");
System.out.println("JWTHEADER : "+ jwtHeader);
//header가 있는지 확인
if(jwtHeader == null || !jwtHeader.startsWith("Bearer")){
chain.doFilter(request, response);
return;
}
//JWT 토큰을 검증해서 정상적인 사용자인지 확인!
String jwtToken = request.getHeader("Authorization").replace("Bearer ", ""); //Bearer로 시작하면, 그 부분을 ""으로 대체
//JWT토큰의 시크릿 키 값인 cos를 가지고 있으면. jwtToken을 서명한다(?)
String username =
JWT.require(Algorithm.HMAC512("cos")).build().verify(jwtToken).getClaim("username").asString();
//서명이 정상적으로 됨
if(username != null){
System.out.println("username 정상");
//Repository에서 username이 존재하는지 조회
User userEntity = userRepository.findByUsername(username);
//조회한 사용자 정보를 바탕으로 PrincipalDetails 생성
//PrincipalDetails : Spring Security의 UserDetails 구현한 사용자 세부정보 객체
PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
//JWT 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
//들어가는 값은 user, password, role
Authentication authentication =
new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
//강제로 시큐리티 세션에 접근해 AUthentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
- 헤더에 있는 jwt를 String jwtToken으로 가져온다
- JWT토큰의 시크릿 키 값을 cos라고 설정했으므로, 알고리즘 HMAC512의 값이 cos이면 build를 시작하고, jwtToken을 값으로 넣어준다. 그리고 jwtToken 속 username을 추출해 String username에 담는다.
- username에 값이 정상적으로 들어왔으면(=null이 아니면) userRepository에 username이 존재하는지 조회한다.
- 조회한 정보 userEntity를 PrincipalDetails(UserDetails 상속)에 넣어준다.
- new UsernamePasswordAuthenticationToken의 인자로 PrincipalDetails, 암호(Pasword), 권한 정보를 넘겨주고 강제로 Authentication을 생성한다.
- 여기에서는 토큰을 이용해 인증하는 방식이기 때문에 암호를 null값을 넣었다.
- 시큐리티 세션에 접근해 Authentication을 저장한다.
- 이후 다시 필터를 타게한다
SecurityConfig 수정
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected final CorsFilter corsFilter;
//IOC 추가
private final UserRepository userRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.addFilterBefore(new MyFilter3(), SecurityContextPersistenceFilter.class);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //세션 없는 상태로 만들겠다.
.and()
.addFilter(corsFilter) //@CrossOrigin는 인증이 없을때만 사용 가능함!, 인증이 필요할 땐 필터에 등록해야한다
.formLogin().disable()
.httpBasic().disable()
.addFilter(new JwtAuthenticationFilter(authenticationManager())) //AuthenticationManager를 파라미터로 꼭 넣어줘야함!!
// userRepository 추가
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userRepository)) //AuthenticationManager를 파라미터로 꼭 넣어줘야함!!
**중간 생략
RestApiController 수정
// user, manager, admin 권한 접근 가능
@GetMapping("/api/v1/user")
public String user(Authentication authentication) {
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println("principalDetails = " + principalDetails.getUsername());
return "user";
}
// manager, admin 접근 가능
@GetMapping("/api/v1/manager")
public String manager() {
return "manager";
}
// admin만 접근 가능
@GetMapping("/api/v1/admin")
public String admin() {
return "admin";
}
이제 다시 포스트맨으로 요청을 보낸다
NullPointerException 발생
Security Session이 생성되지 않아 발생!
에러 발생 원인
- JwtAuthorizationFilter의 doFilterInternal에서 아래처럼 주석 처리한 부분을 주석 처리해주자. 그렇지 않으면
super.doFilterInternal(request, response, chain); 여기서 한번
chain.doFilter(request, response); 여기서 한번
총 두번의 응답이 이뤄져 에러가 발생한다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//아래 부분 주석!!
// super.doFilterInternal(request, response, chain);
System.out.println("인증이나 권한이 필요한 주소 요청 됨");
...... 중간 생략
chain.doFilter(request, response);
}
}
}
수정 후 다시 요청을 보내보면 정상적으로 응답이 오는 것을 확인할 수 있다!
JwtProperties 인터페이스 생성
- 유지보수나 확장성을 생각해서 jwt를 생성할 때 쓰이는 값을들 따로 모아 인터페이스를 생성해주자
public interface JwtProperties {
String SECRET = "cos"; //우리 서버만 알고 있는 비밀 값
int EXPIRATION_TIME = 60000 * 10; // 만료 시간
String TOKEN_PREFIX = "Bearer ";
String HEADER_STRING = "Authorization";
}
JwtAuthenticationFilter 수정
- JwtProperties값을 사용해야 하는 부분을 수정해주자
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication이 실행됨 => 인증이 완료됨");
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
// RSA방식은 아니구 Hash암호방식
// 이 부분 수정
String jwtToken = JWT.create()
.withSubject(principalDetails.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.withClaim("id", principalDetails.getUser().getId())
.withClaim("username", principalDetails.getUser().getUsername())
.sign(Algorithm.HMAC512(JwtProperties.SECRET));
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX+jwtToken);
System.out.println("토큰 생성 후 헤더에 첨부 완료");
// super.successfulAuthentication(request,response,chain,authResult);
}
JwtAuthorizationFilter 수정
- 마찬가지로 JwtProperties값을 사용하는 부분을 수정해주자
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//아래 부분 주석!!
// super.doFilterInternal(request, response, chain);
System.out.println("인증이나 권한이 필요한 주소 요청 됨");
String jwtHeader = request.getHeader(JwtProperties.HEADER_STRING);
System.out.println("jwtHeader = " + jwtHeader);
// header에 토큰이 있는지 확인
if (jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// jwt를 검증해서 정상적인 사용자인지 확인
String jwtToken = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");
// 암호화를 할 때 HMAC512("pem")이라고 넣었으므로 여기서도 pem이라는 단어를 이용해서 복호화 함
String username =
JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(jwtToken).getClaim("username").asString();
// 서명이 정상적으로 되었다는 뜻
if (username != null) {
System.out.println("username 정상");
User userEntity = userRepository.findByUsername(username);
PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
System.out.println("principalDetails = " + principalDetails.getUsername() + " user name이 찍여야 함");
// Jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
Authentication authentication =
new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
// 강제로 시큐리티 세션에 접근하여 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}