이번 프로젝트에서 로그인 및 회원가입 역활을 맡게 되었다. 사실 작업량이 제일 많고 힘든걸 알지만 로그인을 한번 구현해보면 앞으로 다른 팀프로젝트를 진행시 진행이 원할하게 될것 같기도 하고, 뭔가 나 스스로 이정도는 할수 있다는 도전정신..? 으로 맡게 되었다.
일반 로컬 로그인은 JWT를 기반으로 필터를 사용하여 인증 인가를 진행했고 로그인을 구현하였다.
//Dto
@Getter
public class UserSignUpDto {
private String email;
private String password;
private String username;
}
....
//Controller
@PostMapping("/user/signup")
public RestResponse<SampleRes> signUp(@RequestBody UserSignUpDto userSignUpDto) {
try {
userService.signUp(userSignUpDto);
} catch (IllegalArgumentException e) {
return RestResponse.error(ResultCode.SYSTEM_ERROR);
}
return RestResponse.success(SampleRes.builder().name("sign-up").text("회원가입 성공입니다.").build());
}
...
//Service
public void signUp(UserSignUpDto userSignUpDto) {
if (userRepository.findByEmail(userSignUpDto.getEmail()).isPresent()) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
UserEntity user = UserEntity.builder()
.email(userSignUpDto.getEmail())
.username(userSignUpDto.getUsername())
.password(passwordEncoder.encode(userSignUpDto.getPassword()))
.social(UserSocialEnum.LOCAL)
.build();
log.info(user.getEmail());
userRepository.save(user);
}
먼저 회원가입을 할수있는 코드를 구현해 주었다. 이메일을 유니크로 설정해서 중복된 이메일을 가입할수 없게 했다.
//JWT생성
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 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 = "7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==";
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰 생성
public String createToken(String email) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(email) // 사용자 식별자값(ID)
.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();
}
...
//유저정보
public class UserDetailsImpl implements UserDetails {
private final UserEntity user;
public UserDetailsImpl(UserEntity user) {
this.user = user;
}
public UserEntity getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
...
//유저 확인
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserEntity user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + email));
return new UserDetailsImpl(user);
}
}
JwtUtil을 사용하여 jwt형식의 토큰을 이메일을 통해 만들었다. 그리고 UserDetailsImpl와 UserDetailsServiceImpl을 사용해 사용자의 정보를 가져올수 있게 세팅하였다.
//Config
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
//
@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.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers("/v1/user/**").permitAll() //
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
...
//jwt 생성필터
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/v1/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getEmail(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
String email = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getEmail();
String token = jwtUtil.createToken(email);
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
}
...
//검증 필터
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 {
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());
}
}
config와 필터를 사용해서 인증 인가 기능을 구현했다.
로컬 로그인을 전부 구현했지만 이번 우리의 프로젝트 목표는 소셜로그인을 포함한 로그인 기능을 구현해야한다. 그래서 OAuth2를 사용하여서 소셜 로그인을 구현할 계획이다.