Blog

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

Author
Summary
Category
Project
Tags
Spring
Favorite
Memory Date
2023/11/19 → 2023/11/20
Progress Status
Done
Cross Reference Study
Related Media
Related Thought
Related Lessons
tag
날짜
2023/11/19 → 2023/11/20
작성자
진행상황
완료
태그구분
개발
백엔드
5 more properties
📌
전체 과정
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
복사