728x90
✅ 이전 포스팅
2024.11.12 - [Java/쇼핑몰] - 쇼핑몰 - 4 [회원 가입 구현]
이전 포스팅에서는 쇼핑몰의 회원가입을 구현했다.
이번 포스팅에서는 JWT를 이용해 accessToken, refreshToken을 발급하는 내용을 담도록 한다.
이전에 JWT와 mybatis를 이용하 인증/인가에 대해서 작성한 적이 있는데 해당 포스팅에는 accessToken만 언급되고
refreshToken에 대해서는 구현하지 않았는데 이번 기회에 refreshToken을 구현하고 redis와 연결하도록 한다.
✅ Security Config
package toy.shop.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import toy.shop.jwt.JwtAccessDeniedHandler;
import toy.shop.jwt.JwtAuthenticationFilter;
import toy.shop.jwt.JwtExceptionHandler;
import toy.shop.jwt.JwtProvider;
import toy.shop.service.oauth.CustomFailHandler;
import toy.shop.service.oauth.CustomOAuth2UserService;
import toy.shop.service.oauth.CustomSuccessHandler;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final String[] allowedUrls = {
"/swagger-ui/**",
"/v3/**",
"/api/global/**",
"/api/auth/**",
"/images/**"
};
private final JwtProvider jwtProvider;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtExceptionHandler jwtExceptionHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final CustomFailHandler customFailHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화
.formLogin(AbstractHttpConfigurer::disable) // Form 로그인 비활성화
.httpBasic(AbstractHttpConfigurer::disable) // http basic 인증 방식 비활성화
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // X-Frame-Options 설정
// 경로 작업
.authorizeHttpRequests(request -> request
.requestMatchers(allowedUrls).permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasAnyRole("USER", "COMPANY", "ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider, jwtExceptionHandler), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(handling -> handling
.accessDeniedHandler(jwtAccessDeniedHandler))
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
.failureHandler(customFailHandler)
)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // Session 사용하지 않음
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); // 모든 도메인 허용, 필요시 특정 도메인 설정 가능
configuration.setAllowedMethods(List.of("*")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 자격 증명 허용
configuration.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 CORS 설정 적용
return source;
}
}
✅ 로그인
🟨 DTO
🔶 LoginRequestDTO
package toy.shop.dto.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "로그인에 필요한 요청 정보", requiredProperties = {"email", "password"})
public class LoginRequestDTO {
@Schema(description = "회원 이메일")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String email;
@Schema(description = "회원 비밀번호")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String password;
}
🟨 Controller
package toy.shop.controller.global;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import toy.shop.dto.Response;
import toy.shop.dto.jwt.JwtResponseDTO;
import toy.shop.dto.member.LoginRequestDTO;
import toy.shop.service.member.MemberService;
import static toy.shop.controller.ResponseBuilder.buildResponse;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController implements AuthControllerDocs {
private final MemberService memberService;
@PostMapping("/sign-in")
public ResponseEntity<Response<?>> signIn(@RequestBody @Valid LoginRequestDTO parameter) {
JwtResponseDTO result = memberService.login(parameter);
return buildResponse(HttpStatus.OK, "로그인 성공", result);
}
}
🟨 Service
package toy.shop.service.member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import toy.shop.dto.jwt.JwtResponseDTO;
import toy.shop.dto.member.LoginRequestDTO;
import toy.shop.jwt.JwtProvider;
import toy.shop.service.RedisService;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final JwtProvider jwtProvider;
private final RedisService redisService;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final String SERVER = "Server";
/**
* 사용자가 로그인할 때 호출되는 메서드.
* 사용자 인증 후, JWT 액세스 토큰 및 리프레시 토큰을 생성하여 반환합니다.
*
* @param parameter 사용자의 이메일과 비밀번호를 포함하는 로그인 요청 DTO
* @return 생성된 JWT 정보 (액세스 토큰, 리프레시 토큰 포함)
*/
@Transactional
public JwtResponseDTO login(LoginRequestDTO parameter) {
try {
Authentication authentication = authenticateUser(parameter.getEmail(), parameter.getPassword());
String email = authentication.getName();
String authorities = extractAuthorities(authentication);
// 토큰 생성 및 저장
return generateAndStoreToken(SERVER, email, authorities);
} catch (BadCredentialsException e) {
throw new BadCredentialsException("사용자 정보가 잘못 되었습니다.");
}
}
/**
* 사용자 인증을 처리하고 인증된 Authentication 객체를 반환합니다.
* SecurityContext에 인증 정보를 설정합니다.
*
* @param email 사용자의 이메일
* @param password 사용자의 비밀번호
* @return 인증된 사용자 정보가 담긴 Authentication 객체
*/
private Authentication authenticateUser(String email, String password) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(email, password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
/**
* JWT 토큰을 생성하고 리프레시 토큰을 Redis에 저장합니다.
* 기존에 Redis에 리프레시 토큰이 있을 경우 삭제 후 새로운 토큰을 저장합니다.
*
* @param provider 서비스 제공자 정보 (예: "SERVER")
* @param email 사용자 이메일
* @param authorities 사용자의 권한 목록을 콤마로 연결한 문자열
* @return 생성된 JWT 정보 (액세스 토큰 및 리프레시 토큰 포함)
*/
public JwtResponseDTO generateAndStoreToken(String provider, String email, String authorities) {
String redisKey = "RT(" + provider + "):" + email;
// Redis에 기존 RT가 있으면 삭제
Optional.ofNullable(redisService.getValues(redisKey)).ifPresent(rt -> redisService.deleteValues(redisKey));
// 새로운 토큰 생성 및 Redis에 저장
JwtResponseDTO tokenDto = jwtProvider.createToken(email, authorities);
redisService.setValuesWithTimeout(redisKey, tokenDto.getRefreshToken(), jwtProvider.getTokenExpirationTime(tokenDto.getRefreshToken()));
return tokenDto;
}
/**
* Authentication 객체에서 사용자의 권한 목록을 추출하여 콤마로 구분된 문자열로 반환합니다.
*
* @param authentication 사용자 인증 정보가 담긴 Authentication 객체
* @return 사용자의 권한 목록을 콤마로 연결한 문자열
*/
private String extractAuthorities(Authentication authentication) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
}
}
메서드가 잘게 나누어져 있어서 복잡해 보일 수 있지만 JavaDoc을 달아뒀기 때문에 이해하는데 문제는 없으리라 생각된다.
🟨 Repository
package toy.shop.repository.member;
import org.springframework.data.jpa.repository.JpaRepository;
import toy.shop.domain.member.Member;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
/**
* 이메일 주소를 기반으로 회원 정보를 조회하는 메서드입니다.
*
* @param email 조회할 회원의 이메일 주소
* @return 주어진 이메일 주소를 가진 회원을 Optional로 반환하며, 존재하지 않으면 Optional.empty()를 반환합니다.
*/
Optional<Member> findByEmail(String email);
/**
* 주어진 이메일 주소를 가진 회원이 존재하는지 확인하는 메서드입니다.
*
* @param email 존재 여부를 확인할 이메일 주소
* @return 해당 이메일 주소를 가진 회원이 존재하면 true, 존재하지 않으면 false를 반환합니다.
*/
boolean existsByEmail(String email);
}
✅ JWT
🟨 JwtProvider
package toy.shop.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import toy.shop.cmmn.exception.JwtAuthenticationException;
import toy.shop.dto.jwt.JwtResponseDTO;
import toy.shop.service.RedisService;
import java.security.Key;
import java.util.Date;
@Slf4j
@Component
@Transactional(readOnly = true)
public class JwtProvider implements InitializingBean {
private final UserDetailsServiceImpl userDetailsService;
private final RedisService redisService;
private static final String AUTHORITIES_KEY = "role";
private static final String EMAIL_KEY = "email";
private final String secretKey;
private static Key signingKey;
private final Long accessTokenValidityInMilliseconds;
private final Long refreshTokenValidityInMilliseconds;
public JwtProvider(
UserDetailsServiceImpl userDetailsService,
RedisService redisService,
@Value("${jwt.secret}") String secretKey,
@Value("${jwt.access_expire_time}") Long accessTokenValidityInMilliseconds,
@Value("${jwt.refresh_expire_time}") Long refreshTokenValidityInMilliseconds) {
this.userDetailsService = userDetailsService;
this.redisService = redisService;
this.secretKey = secretKey;
this.accessTokenValidityInMilliseconds = accessTokenValidityInMilliseconds;
this.refreshTokenValidityInMilliseconds = refreshTokenValidityInMilliseconds;
}
// 시크릿 키 설정
@Override
public void afterPropertiesSet() throws Exception {
byte[] secretKeyBytes = Decoders.BASE64.decode(secretKey);
signingKey = Keys.hmacShaKeyFor(secretKeyBytes);
}
/**
* 액세스 토큰과 리프레시 토큰을 생성합니다.
*
* @param email 사용자 이메일
* @param authorities 사용자 권한
* @return JwtResponseDTO 액세스 토큰과 리프레시 토큰이 포함된 DTO
*/
@Transactional
public JwtResponseDTO createToken(String email, String authorities){
Long now = System.currentTimeMillis();
String accessToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS512")
.setExpiration(new Date(now + accessTokenValidityInMilliseconds))
.setSubject("access-token")
.claim(EMAIL_KEY, email)
.claim(AUTHORITIES_KEY, authorities)
.signWith(signingKey, SignatureAlgorithm.HS512)
.compact();
String refreshToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS512")
.setExpiration(new Date(now + refreshTokenValidityInMilliseconds))
.setSubject("refresh-token")
.signWith(signingKey, SignatureAlgorithm.HS512)
.compact();
return JwtResponseDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
/**
* 토큰으로부터 Claims 객체를 추출합니다.
*
* @param token JWT 토큰
* @return Claims 토큰에서 추출된 클레임 정보
*/
public Claims getClaims(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) { // Access Token
return e.getClaims();
}
}
/**
* 토큰으로부터 인증 정보를 추출합니다.
*
* @param token JWT 토큰
* @return Authentication 인증 정보
*/
public Authentication getAuthentication(String token) {
String email = getClaims(token).get(EMAIL_KEY).toString();
UserDetailsImpl userDetailsImpl = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetailsImpl, "", userDetailsImpl.getAuthorities());
}
/**
* 토큰의 만료 시간을 반환합니다.
*
* @param token JWT 토큰
* @return long 토큰 만료 시간 (밀리초 단위)
*/
public long getTokenExpirationTime(String token) {
return getClaims(token).getExpiration().getTime();
}
/**
* 리프레시 토큰의 유효성을 검증합니다.
*
* @param refreshToken 리프레시 토큰
* @return boolean 유효한 경우 true, 그렇지 않으면 false
*/
public boolean validateRefreshToken(String refreshToken) {
try {
String redisValue = redisService.getValues(refreshToken);
if ("delete".equals(redisValue)) { // 회원 탈퇴 여부 확인
return false;
}
Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(refreshToken);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature.");
} catch (MalformedJwtException e) {
log.error("Invalid JWT token.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token.");
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty.");
} catch (NullPointerException e) {
log.error("JWT Token is empty.");
}
return false;
}
/**
* HTTP 요청의 Authorization 헤더에서 JWT 토큰을 추출합니다.
*
* @param httpServletRequest HttpServletRequest 객체
* @return String 추출된 토큰 또는 null
*/
public String resolveToken(HttpServletRequest httpServletRequest) {
String bearerToken = httpServletRequest.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
/**
* 액세스 토큰의 유효성을 검증합니다.
*
* @param accessToken 액세스 토큰
* @throws JwtAuthenticationException 토큰이 유효하지 않은 경우 예외 발생
*/
public void validateAccessToken(String accessToken) throws JwtAuthenticationException {
try {
if (redisService.getValues(accessToken) != null // NPE 방지
&& redisService.getValues(accessToken).equals("logout")) { // 로그아웃 했을 경우
throw new JwtAuthenticationException("로그아웃된 토큰입니다.");
}
Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(accessToken);
} catch (SignatureException e) {
log.error("Invalid JWT signature.");
throw new JwtAuthenticationException("Invalid JWT signature.");
} catch (MalformedJwtException e) {
log.error("Invalid JWT token.");
throw new JwtAuthenticationException("Invalid JWT token.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token.");
throw new JwtAuthenticationException("Expired JWT token.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token.");
throw new JwtAuthenticationException("Unsupported JWT token.");
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty.");
throw new JwtAuthenticationException("JWT claims string is empty.");
} catch (NullPointerException e) {
log.error("JWT Token is empty.");
throw new JwtAuthenticationException("JWT Token is empty.");
}
}
/**
* 토큰이 만료되었는지 확인합니다.
*
* @param token JWT 토큰
* @return boolean 만료된 경우 true, 그렇지 않으면 false
*/
public boolean isTokenExpired(String token) {
try {
Date expiration = getClaims(token).getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true; // 만료된 것으로 간주
}
}
}
JWTProvider 또한 내용이 많아서 보기 어려울 수 있기 때문에 JavaDoc을 달아두었습니다.
🟨 JwtAuthenticationFilter
package toy.shop.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import toy.shop.cmmn.exception.JwtAuthenticationException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final JwtExceptionHandler jwtExceptionHandler;
private static final AntPathMatcher pathMatcher = new AntPathMatcher();
private static final List<String> EXCLUDED_URLS = Arrays.asList(
"/swagger-ui/**",
"/v3/**",
"/api/global/**",
"/api/auth/**",
"/images/**"
);
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 제외할 경로일 경우 필터 건너뛰기
String requestURI = request.getRequestURI();
if (EXCLUDED_URLS.stream().anyMatch(pattern -> pathMatcher.match(pattern, requestURI))) {
filterChain.doFilter(request, response);
return;
}
// Access Token 추출
String accessToken = jwtProvider.resolveToken(request);
try {
if (accessToken == null) {
throw new JwtAuthenticationException("토큰이 존재하지 않습니다.");
}
// 정상 토큰인지 검사
jwtProvider.validateAccessToken(accessToken);
Authentication authentication = jwtProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtAuthenticationException e) {
SecurityContextHolder.clearContext();
// 예외를 핸들러로 전달하기 위해 필터 체인 진행 차단
jwtExceptionHandler.commence(request, response, e);
return;
}
filterChain.doFilter(request, response);
}
}
SecurityConfig에서 제외 URL을 설정해도 JWT Filter에서 걸려서 URL 제외를 설정 한 뒤 토큰을 검사하고
토큰일 올바르지 않다면 예외처리를 진행하였다.
🟨 JwtExceptionHandler
package toy.shop.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import toy.shop.dto.Response;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtExceptionHandler implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Response<?> result = Response.builder()
.status(HttpStatus.UNAUTHORIZED.value())
.message(authException.getMessage())
.data(null)
.build();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // application/json
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
🟨 JwtAccessDeniedHandler
package toy.shop.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import toy.shop.dto.Response;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Response<?> result = Response.builder()
.status(HttpStatus.FORBIDDEN.value())
.message("해당 리소스에 접근할 권한이 없습니다.")
.data(null)
.build();
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // application/json
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
🟨 UserDetail
🔶 UserDetailsImpl
package toy.shop.jwt;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import toy.shop.domain.Role;
import toy.shop.domain.member.Member;
import java.util.ArrayList;
import java.util.Collection;
public class UserDetailsImpl implements UserDetails {
private final Member member;
public UserDetailsImpl(Member member) {
this.member = member;
}
public Long getUserId() {
return member.getId();
}
public Role getRole() {
return member.getRole();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> member.getRole().getRole()); // key: ROLE_권한
return authorities;
}
@Override
public String getUsername() {
return member.getEmail();
}
@Override
public String getPassword() {
return member.getPassword();
}
// == 세부 설정 == //
@Override
public boolean isAccountNonExpired() { // 계정의 만료 여부
return true;
}
@Override
public boolean isAccountNonLocked() { // 계정의 잠김 여부
return true;
}
@Override
public boolean isCredentialsNonExpired() { // 비밀번호 만료 여부
return true;
}
@Override
public boolean isEnabled() { // 계정의 활성화 여부
return true;
}
}
🔶 UserDetailsServiceImpl
package toy.shop.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import toy.shop.domain.member.Member;
import toy.shop.repository.member.MemberRepository;
@Component
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetailsImpl loadUserByUsername(String email) throws UsernameNotFoundException {
Member findUser = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다. : " + email));
if(findUser != null){
UserDetailsImpl userDetails = new UserDetailsImpl(findUser);
return userDetails;
}
return null;
}
}
✅ Redis
package toy.shop.service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, String> redisTemplate;
public void setValues(String key, String value){
redisTemplate.opsForValue().set(key, value);
}
// 만료시간 설정 -> 자동 삭제
public void setValuesWithTimeout(String key, String value, long timeout){
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MILLISECONDS);
}
public String getValues(String key){
return redisTemplate.opsForValue().get(key);
}
public void deleteValues(String key) {
redisTemplate.delete(key);
}
}
✅ Test
🟨 로그인 실패
🟨 로그인 성공
🟨 Redis 정보 저장
✅ 마무리
이번 포스팅은 기본적인 로그인의 비즈니스 로직과 해당 로직에서 활용되는 JWT 로직, Redis 로직에 대해 알아봤다.
OAuth2 로그인도 함께 작성할까 했지만 내용이 길어지는 관계로 다음 포스팅에...
'Java > 쇼핑몰' 카테고리의 다른 글
쇼핑몰 7 - [이메일로 비밀번호 변경 URL 전송] (0) | 2024.12.13 |
---|---|
쇼핑몰 - 6 [Spring Security + JWT + OAuth2 + Redis] (2) | 2024.12.02 |
쇼핑몰 - 4 [회원 가입 구현] (0) | 2024.11.12 |
쇼핑몰 - 3 [GitHub Actions를 통한 CI/CD] (3) | 2024.11.06 |
쇼핑몰 - 2 [Docker-compose 사용하기] (0) | 2024.11.03 |