[Java/Spring] - Spring Security 로그인

2023. 10. 31. 20:27· 웹/스프링
목차
  1. 📌-  Spring SecurityConfig
  2. 📌 - User
  3. 📌 - Controller
  4. 📌 - Repository
  5. 📌 - Service
  6. 📌 - Request

 

시작에 앞서

백엔드를 시작한지 그렇게 오래되지 않은 상태로 프로젝트를 할 때, 로그인 기능을 맡아서 했는데 프로젝트가 짧은 기간이었고 지식이 얕은 관계로 시간이 너무 오래걸려서 소셜로그인(카카오) + Jwt(refresh토큰 재발행 하는 로직 없음) 정도로만 구현하고 넘어갔기 때문에 이번에 스터디를 하면서 확실하게 전부 다 구현해보자라는 생각을 했고 적용하면서 나중에 보기 편하게끔 기록해놓을 생각이다.

 

Spring Security 로그인을 시작으로 Jwt를 적용하고 Oauth(구글)을 적용 해볼 생각이다.

 

📌-  Spring SecurityConfig

제일 처음 애먹은 SecurityConfig인데 버전이 계속 바뀌면서 많이 바뀌어서 이전에는 WebSecurityConfigurerAdapter를 사용했었다면 이젠 SecurityFilterChain을 직접 빈으로 등록해서 사용해야 한다.

 


  
import com.example.UsedTrade.auth.configure.auth.*;
import com.example.UsedTrade.auth.util.oauth.CustomOAuth2UserService;
import com.example.UsedTrade.auth.util.oauth.OAuth2LoginFailureHandler;
import com.example.UsedTrade.auth.util.oauth.OAuth2LoginSuccessHandler;
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.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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtFilter jwtFilter;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 인가(접근권한) 설
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/user/*","/").permitAll()
.anyRequest().authenticated())
// Jwt와 Oauth부분은 일단 주석처리
// .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
// .exceptionHandling(excep -> excep
// .accessDeniedHandler(jwtAccessDeniedHandler)
// .authenticationEntryPoint(jwtAuthenticationEntryPoint))
// .oauth2Login(oauth-> oauth
// .successHandler(oAuth2LoginSuccessHandler)
// .failureHandler(oAuth2LoginFailureHandler)
// .userInfoEndpoint(userInfo->userInfo.userService(customOAuth2UserService)))
// .build();
}
}

 

이런식으로 설정을 하고 requestMatchers에는 Security에 걸리지 않고 접속 할 수 있도록 엔드포인트를 설정해준다.

 

 

📌 - User

UserEntity

기본적으로 필요한 정보들로 UserEntity를 만들었다.


  
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
import java.util.*;
@Entity
@Getter
@Builder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EntityListeners(AuditingEntityListener.class)
@Table(name = "users")
public class UserEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(nullable = false)
private String email;
@Column
private String password;
@Column
private String nickname;
@Column
private String imageUrl; // 프로필 이미지
@Column
private int age;
@Column
private String city; // 사는 도시
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
private SocialType socialType; // KAKAO, NAVER, GOOGLE
private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null)
private String refreshToken; // 리프레시 토큰
@CreationTimestamp
private LocalDateTime createdAt;
@CreationTimestamp
private LocalDateTime updatedAt;
// 비밀번호 암호화 메소드
public void passwordEncode(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
}
public void updateRefreshToken(String updateRefreshToken) {
this.refreshToken = updateRefreshToken;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(nickname));
return authorities;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}

 

Role

Oauth를 사용할 때 Role이 필요해서 미리 작성해놓음

 


  
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}

 

 

SocialType


  
public enum SocialType {
KAKAO, NAVER, GOOGLE;
}

 

 


 

📌 - Controller

간단하게 가입과 로그인 정도만 만들어놨고, 아래의 Token 부분은 나중에 Jwt를 구현할 때 사용하므로 지금 단계에선 필요한 값만 가져와서 사용하면 된다.


  
import com.example.UsedTrade.auth.model.Token;
import com.example.UsedTrade.auth.model.request.JoinRequest;
import com.example.UsedTrade.auth.model.request.LoginRequest;
import com.example.UsedTrade.auth.service.UserService;
import com.example.UsedTrade.support.ApiResponse;
import com.example.UsedTrade.support.ApiResponseGenerator;
import com.example.UsedTrade.support.MessageCode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor // final이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 롬복 어노테이션
@RequestMapping("/api/v1/user")
public class LoginController
{
private final UserService userService;
@PostMapping("/join")
public ApiResponse<ApiResponse.SuccessBody<Void>> join(@RequestBody JoinRequest request) throws Exception {
userService.join(request);
return ApiResponseGenerator.success(HttpStatus.OK, MessageCode.SUCCESS);
}
@PostMapping("/login")
public ApiResponse<ApiResponse.SuccessBody<Token>> login(@RequestBody LoginRequest request) {
Token token = userService.login(request);
return ApiResponseGenerator.success(token,HttpStatus.OK, MessageCode.SUCCESS);
}
}

 

 

 

📌 - Repository

DB에서 이메일과 닉네임, 토큰으로 유저를 찾을 수 있도록 Repository 설정


  
import com.example.UsedTrade.auth.entity.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.UsedTrade.auth.entity.UserEntity;
import java.util.*;
public interface UserRepository extends JpaRepository<UserEntity,Long> {
Optional<UserEntity> findByEmail(String Email);
Optional<UserEntity> findByNickname(String nickname);
Optional<UserEntity> findByRefreshToken(String refreshToken);
Optional<UserEntity> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
}

 

 

📌 - Service


  
import com.example.UsedTrade.auth.configure.auth.TokenProvider;
import com.example.UsedTrade.auth.entity.Role;
import com.example.UsedTrade.auth.entity.UserEntity;
import com.example.UsedTrade.auth.model.Token;
import com.example.UsedTrade.auth.model.request.JoinRequest;
import com.example.UsedTrade.auth.model.request.LoginRequest;
import com.example.UsedTrade.auth.repository.UserRepository;
import com.example.UsedTrade.auth.util.UserConverter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
@Service
public class UserService {
private final RedisTemplate<String, String> redisTemplate;
private final UserRepository userRepository;
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final PasswordEncoder passwordEncoder;
@Value("${spring.auth.jwt.refresh_time}")
private Long REFRESH_TOKEN_EXPIRE_TIME;
public void join(JoinRequest request) throws Exception {
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new Exception("이미 존재하는 이메일입니다.");
}
if (userRepository.findByNickname(request.getNickname()).isPresent()) {
throw new Exception("이미 존재하는 닉네임입니다.");
}
UserEntity user = UserEntity.builder()
.email(request.getEmail())
.password(request.getPassword())
.nickname(request.getNickname())
.age(request.getAge())
.city(request.getCity())
.role(Role.USER)
.build();
user.passwordEncode(passwordEncoder);
userRepository.save(user);
}
public Token login(LoginRequest request) {
UserEntity entity =userRepository.findByEmail(request.getEmail()).orElseThrow(() -> new IllegalArgumentException("가입되지 않은 E-MAIL 입니다."));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(request.getEmail(),request.getPassword());
String username = authenticationToken.getName();
Token token = tokenProvider.generateToken(entity);
redisTemplate.opsForValue().set(
username,
token.getRefreshToken(),
REFRESH_TOKEN_EXPIRE_TIME,
TimeUnit.MILLISECONDS
);
return token;
}
public String redisTest(String token){
Authentication authentication = tokenProvider.getAuthentication(token);
String refreshToken = (String) redisTemplate.opsForValue().get(authentication.getName());
return refreshToken;
}
}

 

join - 가입할 때 DB에서 해당 이메일이나 닉네임이 존재하는지 여부를 확인 후에 없으면 DB에 저장하도록 함

login - DB에서 이메일로 해당 유저가 있는지 확인하고 해당 유저의 토큰을 반환 위의 코드에서는 refreshToken을 Redis에 넣어서 관리하려고 redisTemplate를 사용했는데 Redis를 사용하지 않는다면 지워도 되는 부분이다.

redisTest - 토큰이 redis에 잘 저장됐는지 확인 용도

 

 

 

📌 - Request

 


  
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class JoinRequest {
private String email;
private String password;
private String nickname;
private int age;
private String city;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

 

 


  
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {
private String email;
private String password;
}

 

 

전체 코드는

https://github.com/wnstn819/UserTrade

 

GitHub - wnstn819/UserTrade: 중고거래 서비스 구현

중고거래 서비스 구현. Contribute to wnstn819/UserTrade development by creating an account on GitHub.

github.com

여기서 확인 할 수 있다.

 


Reference

https://ksh-coding.tistory.com/57

 

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login(구글, 네이버, 카카오) API 구현 (1) - 회원(User) 관

들어가기 전 처음 프로젝트 진행 시, 아무것도 모르던 상태에서 처음으로 만들어야겠다고 생각이 든 기능이 로그인 기능이었습니다. 처음부터 자체 Login과 OAuth2 Login(소셜 로그인)을 같이 구현해

ksh-coding.tistory.com

 

거의 대부분의 코드를 이 블로그에서 많이 도움을 받았고 설명도 되게 잘 되어있어서 처음 로그인 만들때 참고를 하면 좋을 것 같다고 생각한다.

  1. 📌-  Spring SecurityConfig
  2. 📌 - User
  3. 📌 - Controller
  4. 📌 - Repository
  5. 📌 - Service
  6. 📌 - Request
'웹/스프링' 카테고리의 다른 글
  • [Spring] - URL Shortener 설계 - (2) - 구현
  • [Spring] - URL Shortener 설계 - (1) - 정리
  • [Spring] annotation이란? 왜 쓸까?
  • [Spring] Spring이란?
Casteira
Casteira
할 뿐
Casteira
SpongeCake
Casteira
전체
오늘
어제
  • __Main__ (104)
    • 알고리즘 (65)
      • 개념 (6)
      • 문제 (58)
    • 컴퓨터 구조 (9)
      • 자료 구조 (2)
      • OS (7)
    • 웹 (1)
      • 자바 (1)
      • 스프링 (5)
      • SQL (0)
    • 기록 (4)
      • 포트폴리오 (2)
    • 정글 (18)
      • TIL (17)

블로그 메뉴

  • 🗒️ 깃허브
  • 태그
  • 방명록
  • 관리

공지사항

인기 글

태그

  • 정글
  • java
  • 백준 골드
  • annotation
  • 코딩테스트
  • springboot
  • 크래프톤
  • 백준
  • dp
  • framework
  • 크래프톤 정글
  • spring

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.1
Casteira
[Java/Spring] - Spring Security 로그인
테마상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.