email을 username(id) 로 설정함
//jwt
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
jwt:
secret:
key: 7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
package study.wonyshop.security.service;
@Getter
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails {
private User user;
private String email;//username 즉 id로 email 사용함
public UserDetailsImpl(User user, String email) {
this.user = user;
this.email = email;
}
@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); // 권한을 simpleGrantedAuthority로 추상화하여 관리함
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
/**
* 이메일을 통해 유저정보를 담은 UserDetails 을 반환하는 서비스
*/
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = findByEmail(email);
return new UserDetailsImpl(user,user.getEmail());
}
private User findByEmail(String email) {
return userRepository.findByEmail(email).orElseThrow(
()->new CustomException(ExceptionStatus.WRONG_EMAIL)
);
}
}
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
private static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
private static final String BEARER_PREFIX = "Bearer ";
private static final long ACCESS_TOKEN_TIME =
1000 * 60 * 30L; // 30 분 1000ms(=1s) *60=(1min)*30 =(30min)
private static final long REFRESH_TOKEN_TIME = 1000 * 60 * 60 * 24 * 7L;// 7일
@Value("${jwt.secret.key}")
private String secretKey;
//HMAC-SHA 키를 생성
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
private final UserDetailsService userDetailsService;
private final RedisDao redisDao;
// 이 코드는 HMAC-SHA 키를 생성하는 데 사용되는 Base64 인코딩된 문자열을 디코딩하여 키를 초기화하는 용도로 사용
@PostConstruct//의존성 주입이 이루어진 후 초기화를 수행하는 어노테이션
public void init() {
byte[] bytes = Base64.getDecoder()
.decode(secretKey);// Base64로 인코딩된 값을 시크릿키 변수에 저장한 값을 디코딩하여 바이트 배열로 변환
//* Base64 (64진법) : 바이너리(2진) 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII문자로 표현하기 위해 만들어진 인코딩
key = Keys.hmacShaKeyFor(
bytes);//디코팅된 바이트 배열을 기반으로 HMAC-SHA 알고르즘을 사용해서 Key객체로 반환 , 이를 key 변수에 대입
}
/**
* Header 에서 토큰 가져오기
*
* @param request
* @return
*/
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
/**
* 토큰 생성 메서드 username 대신 email
*
* @param role
* @param tokenExpireTime
* @return Token
*/
private String createToken(String email, UserRoleEnum role, Long tokenExpireTime) {
Date date = new Date();
return BEARER_PREFIX + Jwts.builder()
.claim(AUTHORIZATION_KEY, role)// JWT에 사용자 역할 정보를 클레임(claim)으로 추가합니다.
.setSubject(email)//JWT의 주제(subject)를 사용자 이름으로 설정합니다.
.setIssuedAt(date)
.setExpiration(new Date(date.getTime() + tokenExpireTime))
.signWith(key, SignatureAlgorithm.HS256) //: JWT에 서명을 추가합니다. key는 서명에 사용되는 비밀 키이며,
.compact();
}
/**
* 유저 로그인 후 토큰 발행 username 대신 email
*
* @param role
* @return 에세스토큰과 리프레쉬토큰을 담은 DTO 반환
*/
public TokenResponse createTokenByLogin(String email, UserRoleEnum role) {
String accessToken = createToken(email, role, ACCESS_TOKEN_TIME);
String refreshToken = createToken(email, role, REFRESH_TOKEN_TIME);
redisDao.setRefreshToken(email, refreshToken, REFRESH_TOKEN_TIME);
return new TokenResponse(accessToken, refreshToken);
}
//AccessToken 재발행 + refreshToken 함께 발행
public TokenResponse reissueAtk(String email, UserRoleEnum role, String reToken) {
// 레디스 저장된 리프레쉬토큰값을 가져와서 입력된 reToken 같은지 유무 확인
if (!redisDao.getRefreshToken(email).equals(reToken)) {
throw new CustomException(
ExceptionStatus.AUTHENTICATION);
}
String accessToken = createToken(email, role, ACCESS_TOKEN_TIME);
String refreshToken = createToken(email, role, REFRESH_TOKEN_TIME);
redisDao.setRefreshToken(email, refreshToken, REFRESH_TOKEN_TIME);
return new TokenResponse(accessToken, refreshToken);
}
/**
* 토큰으로 유저정보 가져오기
* @param token
* @return
*/
public Claims getUserInfoFromToken(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
/** Header에서 가져온 토큰 검증하는 메소드
*
* @param token
* @return
*/
public boolean validateToken(String token) {
try {
//parser : parsing을 하는 도구. parsing : token에 내재된 자료 구조를 빌드하고 문법을 검사한다.
//Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)는
// 주어진 토큰을 파싱하기 위해 JWT 파서를 설정하고, 서명 키를 설정한 뒤, 토큰을 파싱하여 JWT 서명 검사를 수행합니다.
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true; // 유효하면 true
} catch (SecurityException | MalformedJwtException |
UnsupportedJwtException e) {// 전: 권한 없다면 발생 , 후: JWT가 올바르게 구성되지 않았다면 발생
log.info("Invalid JWT token, 만료된 jwt 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
/**
* 남은 에세스토큰의 만료시간 조회
* @param accessToken
* @return
*/
public Long getExpiration(String accessToken){
//에세스 토큰 만료시간
Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody()
.getExpiration();
//현재시간
long now = new Date().getTime();
return (expiration.getTime()-now);
}
/**
* 일반 유저 인증 객체 생성
* @param email
* @return
*/
public Authentication createUserAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final RedisDao redisDao;
private final JwtProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String token = jwtProvider.resolveToken(request);
if (token != null) {
String blackList = redisDao.getBlackList(token);
if (blackList != null) {
if (blackList.equals("logout")) {
throw new IllegalArgumentException("Please Login again.");
}
}
if(!jwtProvider.validateToken(token)){
response.sendError(401, "만료되었습니다.");
jwtExceptionHandler(response,"401", HttpStatus.BAD_REQUEST.value() );
return;
}
// 검증 후 인증 객체 생성하여 securityContextHolder에서 관리
Claims userInfo = jwtProvider.getUserInfoFromToken(token);
setAuthentication(userInfo.getSubject());//subject = email
}
filterChain.doFilter(request,response);
}
private void setAuthentication(String email ) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = jwtProvider.createUserAuthentication(email);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
try {
String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
//, ObjectMapper를 사용하여 SecurityExceptionDto 객체를 JSON 문자열로 변환
response.getWriter().write(json); //JSON 문자열을 응답으로 작성
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public class TokenResponse {
private final String accessToken;
private final String refreshToken;
public TokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
//@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
@EnableMethodSecurity // 위 어노테이션은 Deprecated
@EnableScheduling // @Scheduled 어노테이션 활성화
public class WebSecurityConfig implements WebMvcConfigurer {
private final JwtProvider jwtProvider;
private final RedisDao redisDao;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// h2-console 사용 및 resources 접근 허용 설정
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toH2Console())
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
/**
* 필터를 타고 세션이 아닌 jwt방식을 타고 권한을
* 확인하는 메소드
* 스프링부트 3,0 미만은 .antMatchers() 로,
* 3.0 이상은 .requestMatchers() 로 해야 오류가 발생하지 않음
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeHttpRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/api/users/signup").permitAll()
.antMatchers("/api/users/login").permitAll()
// .antMatchers("/owner/**").hasAnyRole("Owner", "Manager", "GeneralManager")
// .requestMatchers("/api/general/**").hasRole("GeneralManager")
.anyRequest().authenticated()//인증이 되어야 한다는 이야기이다.
.and()
// JWT 인증/인가를 사용하기 위한 설정
.addFilterBefore(new JwtAuthFilter(redisDao, jwtProvider),
UsernamePasswordAuthenticationFilter.class);
// xss 공격을 막기위한 필터 설정
// .addFilterBefore(new XssEscapeFilter(xssEscapeUtil), CsrfFilter.class)
// 401 Error 처리, Authorization 즉, 인증과정에서 실패할 시 처리
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
// 403 Error 처리, 인증과는 별개로 추가적인 권한이 충족되지 않는 경우
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
return http.build();
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD")
.allowedOriginPatterns("*")
.exposedHeaders("Authorization");
}
}
@Getter
@NoArgsConstructor
public class SecurityExceptionDto {
private int statusCode;
private String msg;
public SecurityExceptionDto(int statusCode, String msg) {
this.statusCode = statusCode;
this.msg = msg;
}
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public class SignUpRequest {
// 회원가입 비밀번호 재확인 추가
// 정규식이 틀렸을때 발생하는 예외 MethodArgumentNotValidException
// @NotBlank = null 과 "" 과 " " 모두 비허용, @Notnull = "" 이나 " " 은 허용, @NotEmpty = null 과 "" 은 불가, " " 은 허용
@Email
private final String email;
@Pattern(regexp = "[a-zA-Z0-9]{4,8}$", message = "닉네임은 최소 4자 이상, 8자 이하이며, 영문과 숫자만 입력하세요.")
private final String nickName;
@Pattern(regexp = "(?=.*[a-zA-Z])(?=.*[0-9])^[a-zA-Z0-9~!@#$%^&*()+|=]{8,15}$", message = "비밀번호는 최소 8자 이상, 15자 이하이며, 영문과 숫자, 특수문자만 입력하세요.")
private final String password;
@Pattern(regexp = "(?=.*[a-zA-Z])(?=.*[0-9])^[a-zA-Z0-9~!@#$%^&*()+|=]{8,15}$", message = "비밀번호를 확인해주세요.")
private final String password2;
@Pattern(regexp = "(?=.*[0-9])^[0-9]{11}$", message = "-을 제외한 10자리 번호를 입력해주세요")
private final String phoneNumber;
@NotBlank
private final String address;
// private boolean admin = false;
//
// private String adminToken = "";
@Builder
public SignUpRequest(String email, String nickName, String password, String password2,
String phoneNumber, String address) {
this.email = email;
this.nickName = nickName;
this.password = password;
this.password2 = password2;
this.phoneNumber = phoneNumber;
this.address = address;
}
/**
* DTO -> Entity
* @param role
* @param encodedPassword
* @return
*/
public User toEntity(UserRoleEnum role, String encodedPassword) {
return User.builder()
.nickname(nickName)
.email(email)
.phoneNumber(phoneNumber)
.password(encodedPassword)
.address(address)
.role(role)
.build();
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public class LoginRequest {
private final String email;
private final String password;
public LoginRequest(String email, String password) {
this.email = email;
this.password = password;
}
}
@Getter
@NoArgsConstructor(force = true)
public class UserResponse {
private final String email;
private final String nickname;
private final String phoneNumber;
private final UserRoleEnum role;
private final String address;
private final String profileImage;
/**
* 유저 생성자를 private로 외부에서 생성 할수 없도록 함
* @param user
*/
private UserResponse(User user) {
this.email = user.getEmail();
this.nickname = user.getNickname();
this.phoneNumber = user.getPhoneNumber();
this.role = user.getRole();
this.address = user.getAddress();
this.profileImage = user.getProfileImage();
}
/**
* 유저 생성자를 private로 외부에서 생성 할수 없도록 함으로 써
* of 메서드를 통해
* 유저 객체를 DTO에 담아 반환해줍니다.
* @param user
* @return
*/
public static UserResponse of(User user){
return new UserResponse(user);
}
}
package study.wonyshop.security.dto;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public class ReissueTokenRequest {
private final String refreshToken;
public ReissueTokenRequest(String refreshToken) {
this.refreshToken = refreshToken;
}
}
@Entity
@Table(name = "USERS") //테이블 user 예약어 있어서 사용할 수 없음)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id", nullable = false)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false, unique = true)
private String nickname;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String address;
@Setter
private String profileImage;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
@Column(nullable = false, unique = true)
private String phoneNumber;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
private Boolean inUser ; // 추후 휴면계정 관리 할때 사용 하기 위함
@Builder
public User(String email, String nickname, String password, String address, String profileImage,
UserRoleEnum role, String phoneNumber) {
this.email = email;
this.nickname = nickname;
this.password = password;
this.address = address;
this.profileImage = profileImage;
this.role = role;
this.phoneNumber = phoneNumber;
}
}
@Getter
public enum UserRoleEnum {
ADMIN(Authority.ADMIN),
MEMBER(Authority.MEMBER),
SELLER(Authority.SELLER);
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public static class Authority{
public static final String ADMIN = "ROLE_ADMIN";
public static final String SELLER = "ROLE_SELLER";
public static final String MEMBER = "ROLE_MEMBER";
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final JwtProvider jwtProvider;
/**
* 회원가입
*
* @param signUpRequest
* @return 회원가입 성공
*/
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody @Valid SignUpRequest signUpRequest) {
//패스워드 1과 패스워드 2 가 동일 한지 체크
if (!signUpRequest.getPassword().equals(signUpRequest.getPassword2())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다. 다시 입력해주세요.");
}
return userService.signup(signUpRequest);
}
/**
* 로그인
* 로그인 시 atk, rtk 이 생성되어 response header 에 담아 보낸다.
* rtk 는 레디스에 저장한다. 추후 atk 만료시 rtk를 이용해 atk 재발급 하기 위함
*/
@PostMapping("/login")
public TokenResponse login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
UserResponse user = userService.login(loginRequest);
TokenResponse token = jwtProvider.createTokenByLogin(user.getEmail(),
user.getRole());//atk, rtk 생성
response.addHeader(jwtProvider.AUTHORIZATION_HEADER, token.getAccessToken());// 헤더에 에세스 토큰만 싣기
return token;
}
/**
* 로그아웃
* 현 accessToken 은 다시 사용하지 못하도록 레디스에 저장해두고,
* 로그아웃시 레디스에 저장된 refreshToken 삭제
* @param userDetails
* @param request
* @return
*/
@DeleteMapping("/logout")
public ResponseEntity logout(@AuthenticationPrincipal UserDetailsImpl userDetails, HttpServletRequest request){
String accessToken = jwtProvider.resolveToken(request);
return userService.logout(accessToken,userDetails.getUsername());//username = email
}
/**
* 해당 유저의 정보 확인
* @param userDetails
* @return
*/
@GetMapping("/user-info")
public UserResponse getUserInfo(@AuthenticationPrincipal UserDetailsImpl userDetails){
return userService.getUserInfo(userDetails.getUsername());//username = email
}
/**
* AccessToken 재발급
* 매 API 호출 시 시큐리티필터를 통해 인증인가를 받게 된다. 이때 만료된 토큰인지 검증하고 만료시 만료된토큰임을 에러메세지로 보낸다.
* 그럼 클라이언트에서 에러메세지를 확인 후 이 api(atk 재발급 ) 을 요청 하게 된다.
* @param userDetails
* @param tokenRequest : refreshToken
* @return AccessToken + RefreshToken
*/
@PostMapping("/reissue-token")
public TokenResponse reissueToken(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestBody ReissueTokenRequest tokenRequest){
//유저 객체 정보를 이용하여 토큰 발행
UserResponse user = UserResponse.of(userDetails.getUser());
return jwtProvider.reissueAtk(user.getEmail(),user.getRole(), tokenRequest.getRefreshToken());
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final RedisDao redisDao;
/**
* 회원가입
*
* @param signUpRequest
* @return
*/
@Transactional
public ResponseEntity signup(@Valid SignUpRequest signUpRequest) {
String email = signUpRequest.getEmail();
String nickname = signUpRequest.getNickName();
String password = passwordEncoder.encode(signUpRequest.getPassword());
String phoneNumber = signUpRequest.getPhoneNumber();
String address = signUpRequest.getAddress();
//닉네임 중복 확인
Optional<User> findNickname = userRepository.findByNickname(nickname);
if (findNickname.isPresent()) {
throw new CustomException(ExceptionStatus.DUPLICATED_NICKNAME);
}
// 이메일 중복 확인
Optional<User> findEmail = userRepository.findByEmail(email);
if (findEmail.isPresent()) {
throw new CustomException(ExceptionStatus.DUPLICATED_EMAIL);
}
// 폰번호 중복 확인
Optional<User> findPhoneNumber = userRepository.findByPhoneNumber(phoneNumber);
if (findPhoneNumber.isPresent()) {
throw new CustomException(ExceptionStatus.DUPLICATED_PHONENUMBER);
}
UserRoleEnum role = UserRoleEnum.MEMBER;
User user = signUpRequest.toEntity(role, password);
user.setProfileImage("default.png");
userRepository.save(user);
return ResponseEntity.ok("회원가입 성공");
}
/**
* 로그인 반환값으로 user를 userResponseDto 담아 반환하고 컨트롤러에서 반환된 객체를 이용하여 토큰 발행한다.
*/
@Cacheable(cacheNames = CacheNames.LOGINUSER, key = "'login'+ #p0.getEmail()", unless = "#result== null")
@Transactional
public UserResponse login(LoginRequest loginRequest) {
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();
User user = userRepository.findByEmail(email).orElseThrow(
() -> new CustomException(ExceptionStatus.WRONG_EMAIL)
);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new CustomException(ExceptionStatus.WRONG_PASSWORD);
}
return new UserResponse().of(user);// user객체를 dto에 담아서 반환
}
@CacheEvict(cacheNames = CacheNames.USERBYEMAIL, key = "'login'+#p1")
@Transactional
public ResponseEntity logout(String accessToken, String email) {
// 레디스에 accessToken 사용못하도록 등록
Long expiration = jwtProvider.getExpiration(accessToken);
redisDao.setBlackList(accessToken, "logout", expiration);
if (redisDao.hasKey(email)) {
redisDao.deleteRefreshToken(email);
} else {
throw new IllegalArgumentException("이미 로그아웃한 유저입니다.");
}
return ResponseEntity.ok("로그아웃 완료");
}
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByNickname(String nickname);
@Cacheable(cacheNames = CacheNames.USERBYEMAIL, key = "'login'+#p0", unless = "#result==null")
Optional<User> findByEmail(String email);
Optional<User> findByPhoneNumber(String phoneNumber);
}
로그인 시 atk, rtk 이 생성되어 response header 에 담아 보낸다.
rtk 는 레디스에 저장한다. 추후 atk 만료시 rtk를 이용해 atk 재발급과 로그아웃시 레디스에 rtk를 삭제하여 로그아웃을 구현한다.
[Spring] Redis 사용법 + jwt 함께 로그인, 로그아웃 구현
로그인 후 atk, rtk 이 생성되어 response header 에 담아 보낸다.
안녕하세요! 좋은 정보 얻어갑니다!
질문이 있는데,
Key를 생성할때 PostConstruct로 초기화해주는것의 장점이 뭔가요?