-
SpringBoot - JWT ( JSON WEB TOKEN ) - 코드편Spring-Boot/etc. 2022. 5. 9. 15:03
HS512 알고리즘을 사용할 것이기 때문에 secret key는 512bit, 즉 64byte 이상을 사용해야 합니다.
터미널에서 secret key를 base64로 인코딩하여 secret 항목에 채워넣습니다.
$ echo ‘silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret’|base64
application.yml
jwt: header: Authorization secret: 4oCYc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXTigJkK token-validity-in-seconds: 86400
( 저는 그냥 하드 코딩했습니다. 하지만 yml 파일에서 관리하는게 좋습니다. )
Filter방식으로 JWT 토큰 활용하기
1. WebSecurityConfig
- corsFilter는 cors방지용 필터
- JwtAuthenticationFilter : 로그인 성공시 토큰발급해주는 필터
- JwtAuthorizationFilter : 해당 토큰 보안여부 판단해주는 필터
( /joinMember와 /readBoard 이외에는 이 필터가 판단)
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // jwt토큰 방식을 쓰기에 필요한 설정 .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않기에 stateless .and() .addFilter(corsFilter) // @CrossOrigin(인증X), 시큐리티 필터에 등록 인증(O) .formLogin().disable() // 폼로그인 사용X .httpBasic().disable() // Bearer 방식을 쓸거기 때문 .addFilter(new JwtAuthenticationFilter(authenticationManager())) // AuthenticationManager .addFilter(new JwtAuthorizationFilter(authenticationManager(),memberMapper)) .authorizeRequests() .antMatchers("/joinMember","/readBoard").permitAll() //login페이지 .anyRequest().authenticated(); } }
2. CorsConfig
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 내 서버가 응답할 때 json을 JS에서 처리할 수 있게 설정 config.addAllowedOrigin("*"); // 모든 ip에 응답을 허용 config.addAllowedHeader("*"); // 모든 header에 응답 허용 config.addAllowedMethod("*"); // 모든 post,get,put,delete,patch 요청허용 source.registerCorsConfiguration("/**",config); return new CorsFilter(source); } }
3. JwtAuthenticationFilter
// 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter 가 있음 // /login 요청해서 username, password 전송하면 (post) // UsernamePasswordAuthenticationFilter 동작함 @RequiredArgsConstructor public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; // /login요청을 하면 로그인 시도를 위해서 실행되는 함수 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1. username,password 받아서 try { // json데이터를 parsing해줌 ObjectMapper om = new ObjectMapper(); MemberVO mem = om.readValue(request.getInputStream(), MemberVO.class); System.out.println("request요청 값들 :"+mem); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(mem.getM_id(),mem.getM_pw()); // PrincipalDetailsService의 loadUserByUsername() 함수가 실행된 후 정상이면 authentication이 리턴 // DB에 있는 username과 password가 일치한다. // pw는 스프링이 알아서 처리 해줌 Authentication authentication = authenticationManager.authenticate(authenticationToken); // => 로그인이 되었다는 뜻 PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); System.out.println("로그인 완료 여부: "+principalDetails.getUsername()); // authentication 객체가 session영역에 저장을 해야하고 그 방법이 return 해주면 됨 // 리턴의 이유는 권한 관리를 security가 대신 해주기 떄문에 편하려고 하는 거임 // 굳이 JWT토큰을 사용하면서 세션을 만들 이유가 없음. 근데 단지 권한 처리때문에 session넣어줌 return authentication; } catch (IOException e) { e.printStackTrace(); System.out.println(111111); } // 2. 정상인지 로그인 시도를 해보는 것, authenticationManager로 로그인 시도를 하면!! // PrincipalDetailsService가 호출 -> loadUserByUsername이 자동으로 실행 // 3. PrincipalDetails를 세션에 담고 ( 권한 관리를 위해서 ) // 4. JWT토큰을 만들어서 응답해주면 됨. return null; } // attemptAuthentication 실행 후 인증이 정상적으로 되었으면 successfulAuthentication 함수 실행 // 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(); // RSA방식은 아니구 Hash암호방식 String jwtToken = JWT.create() .withSubject("cos토큰") .withExpiresAt(new Date(System.currentTimeMillis()+(60000*10000))) .withClaim("username",principalDetails.getUsername()) .withClaim("m_seq", Integer.toString(principalDetails.getMem().getM_seq())) .sign(Algorithm.HMAC512("cos")); response.addHeader("Authorization","Bearer "+jwtToken); } }
- token의 규칙은 { "Authorization" : "Bearer 토큰명" } 이다.
( 안지키면 token 판별을 못하더라구요.... )
- attemptAuthentication의 authenticationManager.authenticate가 실행 시 PrincipalDetailService의
loadUserByUsername이 실행되므로 다음 파일로 보겠다.
4. PrincipalDetailsService
@Service @RequiredArgsConstructor public class PrincipalDetailsService implements UserDetailsService { private final MemberMapper memberMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("loadUserByUsername 로그인요청"); MemberVO user = memberMapper.selectId(username); return new PrincipalDetails(user); } }
- loadUserByUsername의 반환변수가 UserDetails이므로 PrincipalDetails 파일도 첨부
+ PrincipalDetails
@Data @NoArgsConstructor @AllArgsConstructor public class PrincipalDetails implements UserDetails { private MemberVO mem; @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new GrantedAuthority() { @Override public String getAuthority() { return mem.getM_role(); } }); return authorities; } @Override public String getPassword() { return mem.getM_pw(); } @Override public String getUsername() { return mem.getM_id(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
5. JwtAuthorizationFilter
- 토큰 인증해주는 필터
// 시큐리티가 filter가지고 있는 그 필터중에 BasicAuthenticationFilter 라는 것이 있음 // 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어있음 // 만약에 권한이 인증이 필요한 주소가 아니라면 이 필터를 안탄다. public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private MemberMapper memberMapper; public JwtAuthorizationFilter(AuthenticationManager authenticationManager, MemberMapper memberMapper) { super(authenticationManager); this.memberMapper = memberMapper; } // 인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 타게 됨 @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 : " + jwtHeader); // header가 있는 확인 if (jwtHeader == null || jwtHeader.trim().isEmpty() || !jwtHeader.startsWith("Bearer")) { chain.doFilter(request, response); }else { // JWT 토큰을 검증을 해서 정상적인 사용자인지 확인 String jwtToken = request.getHeader("Authorization").replace("Bearer ", ""); String username = JWT.require(Algorithm.HMAC512("cos")).build().verify(jwtToken).getClaim("username").asString(); // 서명이 정상적으로 됨 if (username != null) { MemberVO mem = memberMapper.selectId(username); PrincipalDetails principalDetails = new PrincipalDetails(mem); // Jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다. Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities()); // 강제로 시큐리티의 세션에 접근하여 Authentication 객체를 저장. SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } } } }
+ Controller에서 토큰 복호화해서 쓰기
@PostMapping("/") public void 토큰복화화(@RequestHeader("Authorization") String jwtToken){ // 토큰 복호화 과정 String token = jwtToken.substring(7); System.out.println("post에서 받은 토큰 : "+token); String m_seq = JWT.require(Algorithm.HMAC512("cos")).build().verify(token).getClaim("m_seq").asString(); String username = JWT.require(Algorithm.HMAC512("cos")).build().verify(token).getClaim("username").asString(); System.out.println("post user : "+m_seq+ " / " + username); }
마무리
Spring-Security와 JWT토큰 적용시키기위해 4일동안 공부하고 코드쓰고... 쉽지 않았다...
@Override때문에 기존의 스프링 라이브러리 읽는 것도 연습해야겠고 자동적으로 실행하는 순서때문에 많이 애먹은거같다. 나중에는 편하다고 느껴지겠지만 지금은 이 자동적으로 실행하는거 때문에 많은 시간을 썼다.
정리하자면
1. 로그인과정 + 토큰 발급
WebScurityConfig.java
-> JwtAuthenticationFilter.java( extends UsernamePasswordAuthenticationFilter ) 의
attemptAuthentication ( @Override )에서 로그인을 처리해주는데 해당 함수에서
-> PrincipalDetailsService( implements UserDetailsService )의 loadUserByUsername( @Override )을 실행하고
-> PrincipalDetails( implements UserDetails )를 반환한다.
-> 로그인을 성공하면 attemptAuthentication( @Override ) 실행 후 바로
successfulAuthentication( @Override )을 실행해서 토큰을 발급해준다.
2. 토큰 인증
WebScurityConfig.java
-> WebScurityConfig.java에서 권한이나 인증이 필요한 특정 주소가 요청이 온다면
JwtAuthorizaionFilter( extends BasicAuthenticationFilter )의
doFilterInternal( @Override )이 실행되고 토큰 인증을 하게 된다.
implements ,extends, @Override 의 중요성을 많이 깨닫고 간다...
다음은 JWT를 인터셉터로 한번해보거나 JWT토큰 코드를 좀 더 리펙토링해봐야겠다.
github : https://github.com/PHyeonMIN/boardDemo
( config만 보시면됩니다. config파일과 같은 라인에있는 jwt는 실험용이었습니다..ㅠ )
'Spring-Boot > etc.' 카테고리의 다른 글
SpringBoot - DB 로그 찍기 (0) 2022.05.11 SpringBoot - CORS 해결방안 (0) 2022.05.09 SpringBoot - JWT ( JSON WEB TOKEN ) - 개념편 (0) 2022.05.09 SpringBoot - Spring Security (0) 2022.05.06 SpringBoot - 스프링 빈의 순환 종속성 문제 (Circular Dependencies in Spring) (0) 2022.05.04