Blog

[Spring][iLanD] MVP - [Auth] SpringSecurity JWT+쿠키 방식 로그인 기능 구현

Category
Author
Tags
PinOnMain
1 more property
전체 과정
1.
의존성 추가
2.
UserDetailsImpl 생성
3.
UserRepository 추가
4.
UserDetailsService 생성
5.
JWT유틸 클래스 추가
6.
JWT필터 추가
7.
스프링 시큐리티 설정
8.
스프링 설정
9.
컨트롤러 설정
추후 Refresh Token 참고 자료
의존성 추가
//JWT compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
Java
복사
UserDetailsImpl.java
public class UserDetailsImpl implements UserDetails { private final User user; public UserDetailsImpl(User user) { this.user = user; } public User getUser() { return user; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { UserRoleEnum role = user.getRole(); String authority = role.getAuthority(); SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority); Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(simpleGrantedAuthority); return authorities; }
Java
복사
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); }
Java
복사
UserDetailsServiceImpl.java
@Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("찾을 수 없는 이메일" + email)); return new UserDetailsImpl(user); } }
Java
복사
JwtUtil.java
@Slf4j(topic = "JwtUtil") @Component public class JwtUtil { // Header KEY 값 public static final String AUTHORIZATION_HEADER = "Authorization"; // 사용자 권한 값의 KEY public static final String AUTHORIZATION_KEY = "auth"; // Token 식별자 public static final String BEARER_PREFIX = "Bearer "; // 토큰 만료시간 private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분 @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey private String secretKey; private Key key; private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 로그 설정 -> addcookie할 때 필요 public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그"); @PostConstruct public void init() { byte[] bytes = Base64.getDecoder().decode(secretKey); key = Keys.hmacShaKeyFor(bytes); } // 토큰 생성 public String createToken(String email, UserRoleEnum role) { // 토큰 생성 -> email 기준으로 Date date = new Date(); return BEARER_PREFIX + Jwts.builder() .setSubject(email) // 사용자 식별자값(ID) .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 .setIssuedAt(date) // 발급일 .signWith(key, signatureAlgorithm) // 암호화 알고리즘 .compact(); } // ***** 이전과 바뀐 부분 헤더에서 가져오기 ******* 쿠키 배열에서 추출하는 것이아니라 헤더에서 가져와서 간결해짐 // header 에서 JWT 가져오기 public String getJwtFromHeader(HttpServletRequest request) { String bearerToken = request.getHeader(AUTHORIZATION_HEADER); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { return bearerToken.substring(7); } return null; } // 토큰 검증 public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (SecurityException | MalformedJwtException | SignatureException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); } catch (ExpiredJwtException e) { log.error("Expired JWT token, 만료된 JWT token 입니다."); } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); } catch (IllegalArgumentException e) { log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); } return false; } // 토큰에서 사용자 정보 가져오기 public Claims getUserInfoFromToken(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); } // -------------------------------------------------------------------------------// // postman 테스트 용 -> 쿠키에 담아서 인증 public void addJwtToCookie(String token, HttpServletResponse res) { try { token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행 Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value cookie.setPath("/"); // Response 객체에 Cookie 추가 res.addCookie(cookie); } catch (UnsupportedEncodingException e) { logger.error(e.getMessage()); } } // HttpServletRequest 에서 Cookie Value : JWT 가져오기 public String getTokenFromRequest(HttpServletRequest req) { Cookie[] cookies = req.getCookies(); if(cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals(AUTHORIZATION_HEADER)) { try { return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode } catch (UnsupportedEncodingException e) { return null; } } } } return null; } // JWT 토큰 substring public String substringToken(String tokenValue) { if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { return tokenValue.substring(7); } logger.error("Not Found Token"); throw new NullPointerException("Not Found Token"); } // 쿠키에서 JWT 제거 public void removeJwtFromCookie(HttpServletResponse res) { Cookie cookie = new Cookie(AUTHORIZATION_HEADER, null); cookie.setMaxAge(0); cookie.setPath("/"); res.addCookie(cookie); } }
Java
복사
JwtAuthenticationFilter.java
@Slf4j(topic = "로그인 및 JWT 생성") public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final JwtUtil jwtUtil; public JwtAuthenticationFilter(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { log.info("로그인 시도"); try { AuthLoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), AuthLoginRequestDto.class); return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( requestDto.getEmail(), requestDto.getPassword(), null ) ); } catch (IOException e) { log.error("예외 발생: ", e); throw new RuntimeException("요청 처리 중 오류가 발생했습니다."); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { log.info("로그인 성공 및 JWT 생성"); String email = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getEmail(); UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole(); String token = jwtUtil.createToken(email, role); jwtUtil.addJwtToCookie(token, response); StatusResponseDto statusResponseDto = new StatusResponseDto("로그인 성공", HttpServletResponse.SC_OK); String jsonResponse = new ObjectMapper().writeValueAsString(statusResponseDto); response.setStatus(HttpServletResponse.SC_OK); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(jsonResponse); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { log.info("로그인 실패"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); String failureMessage = failed.getMessage(); StatusResponseDto statusResponseDto = new StatusResponseDto("로그인 실패: " + failureMessage, HttpServletResponse.SC_UNAUTHORIZED); String jsonResponse = new ObjectMapper().writeValueAsString(statusResponseDto); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(jsonResponse); } }
Java
복사
JwtAuthorizationFilter.java
@Slf4j(topic = "JWT 검증 및 인가") public class JwtAuthorizationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsServiceImpl userDetailsService; public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } // 헤더에 담아서 요청할 때 @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { // *** 이전과 다른부분, 쿠키에서 토큰을 추출하던 것에서 getJwtFromHeader()를 통해 헤더에서 순수한 토큰을 추출하는 것으로 변경 간결해짐. String tokenValue = jwtUtil.getJwtFromHeader(req); if (StringUtils.hasText(tokenValue)) { if (!jwtUtil.validateToken(tokenValue)) { log.error("Token Error"); return; } Claims info = jwtUtil.getUserInfoFromToken(tokenValue); try { setAuthentication(info.getSubject()); } catch (Exception e) { log.error(e.getMessage()); return; } } filterChain.doFilter(req, res); } // 인증 처리 public void setAuthentication(String email) { SecurityContext context = SecurityContextHolder.createEmptyContext(); Authentication authentication = createAuthentication(email); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); } // 인증 객체 생성 private Authentication createAuthentication(String email) { UserDetails userDetails = userDetailsService.loadUserByUsername(email); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } }
Java
복사
WebSecurityConfig.java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final JwtUtil jwtUtil; private final UserDetailsServiceImpl userDetailsService; private final AuthenticationConfiguration authenticationConfiguration; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil); filter.setAuthenticationManager(authenticationManager(authenticationConfiguration)); return filter; } @Bean public JwtAuthorizationFilter jwtAuthorizationFilter() { return new JwtAuthorizationFilter(jwtUtil, userDetailsService); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // CSRF 설정 http.csrf((csrf) -> csrf.disable()); // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정 http.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); http .authorizeRequests(authorizeRequests -> authorizeRequests .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정 .requestMatchers("/api/auth/**").permitAll() // '/api/auth/'로 시작하는 요청 모두 접근 허가 > 회원가입, 로그인 // 메뉴 조회는 누구나 접근 / 카페도 추후 추가 ... ); // 필터 관리 http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
Java
복사
AuthController.java
@RestController @AllArgsConstructor @RequestMapping("/api/auth") public class AuthController { private final JwtUtil jwtUtil; private final AuthenticationManager authenticationManager; // 로그인 @PostMapping("/login") public StatusResponseDto login(@RequestBody AuthLoginRequestDto authLoginRequestDto, HttpServletResponse response) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( authLoginRequestDto.getEmail(), authLoginRequestDto.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); String token = jwtUtil.createToken(userDetails.getUser().getEmail(), userDetails.getUser().getRole()); jwtUtil.addJwtToCookie(token, response); return new StatusResponseDto("로그인 성공", HttpServletResponse.SC_OK); } // 로그아웃 @PostMapping("/logout") public StatusResponseDto logout(HttpServletResponse response) { SecurityContextHolder.clearContext(); // Clear security context jwtUtil.removeJwtFromCookie(response); // Remove JWT from cookie return new StatusResponseDto("로그아웃 성공", HttpServletResponse.SC_OK); } }
Java
복사
AuthViewController.java
@Controller public class AuthViewController { // 로그인화면 @GetMapping("/login") public String loginView(Model model) { SecurityContext securityContextHolder = SecurityContextHolder.getContext(); Object principal = securityContextHolder.getAuthentication().getPrincipal(); String role = ""; if(principal instanceof UserDetailsImpl) role = String.valueOf(((UserDetailsImpl) principal).getUser().getRole()); else role = "ANONYMOUS"; model.addAttribute("loginUserRole", role); return "employee_login"; } }
Java
복사