Skip to content

Commit

Permalink
[JT-29] JWT 로그인 기능 구현
Browse files Browse the repository at this point in the history
[JT-29] JWT 로그인 기능 구현
  • Loading branch information
ymkim97 authored Sep 6, 2023
2 parents 867845d + c29fb67 commit 6b25818
Show file tree
Hide file tree
Showing 19 changed files with 378 additions and 47 deletions.
10 changes: 10 additions & 0 deletions module-application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ dependencies {

// Bean Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.springframework.web.bind.annotation.RestController;

import com.devtoon.jtoon.member.application.MemberService;
import com.devtoon.jtoon.member.request.SignUpDto;
import com.devtoon.jtoon.member.request.SignUpReq;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand All @@ -23,8 +23,8 @@ public class MemberController {

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void signUp(@RequestBody @Valid SignUpDto signUpDto) {
memberService.createMember(signUpDto);
public void signUp(@RequestBody @Valid SignUpReq signUpReq) {
memberService.createMember(signUpReq);
}

@GetMapping("/email-authorization")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.devtoon.jtoon.security.application;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.security.jwt.JwtProvider;
import com.devtoon.jtoon.security.request.LogInReq;
import lombok.RequiredArgsConstructor;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;

@Transactional
public String login(LogInReq logInReq) {
Member member = memberRepository.findByEmail(logInReq.email())
.orElseThrow(() -> new BadCredentialsException("너 안돼!"));

if (!isPasswordSame(logInReq.password(), member.getPassword())) {
throw new BadCredentialsException("너 안돼!");
}

member.updateLastLogin();

return jwtProvider.generateToken(logInReq.email());
}

public boolean isPasswordSame(String rawPassword, String memberPassword) {
return passwordEncoder.matches(rawPassword, memberPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.devtoon.jtoon.security.config;

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.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.servlet.HandlerExceptionResolver;

import com.devtoon.jtoon.security.filter.JwtAuthenticationFilter;
import com.devtoon.jtoon.security.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfiguration {

private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtProvider jwtProvider;

@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(request -> request
.requestMatchers("/members").permitAll()
.requestMatchers("/members/email-authorization").permitAll()
.requestMatchers("/members/**").hasAuthority("USER")
.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthenticationFilter(handlerExceptionResolver, jwtProvider), UsernamePasswordAuthenticationFilter.class)
;
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.devtoon.jtoon.security.filter;

import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import com.devtoon.jtoon.security.jwt.JwtProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(
HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain filterChain
) throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);

if (header != null && header.startsWith("Bearer")) {
try {
String token = header.split(" ")[1];
jwtProvider.validateToken(token);
Authentication auth = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (RuntimeException e) {
log.error("Invalid Token", e);
handlerExceptionResolver.resolveException(request, response, null, e);
return;
}
}
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.devtoon.jtoon.security.jwt;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {

@Value("${jwt.secret.key}")
private String SALT;

@Value("${jwt.iss}")
private String ISS;

@Value("${jwt.expire}")
private long EXPIRE;

private Key secretKey;

private final UserDetailsService userDetailsService;

@PostConstruct
private void init() {
secretKey = Keys.hmacShaKeyFor(SALT.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(String email) {
Claims claims = getClaims(email);

return Jwts.builder()
.setClaims(claims)
.signWith(secretKey, SignatureAlgorithm.HS256)
.setHeader(getHeaders())
.compact();
}

private Map<String, Object> getHeaders() {
Map<String, Object> headers = new HashMap<>();
headers.put("alg", "HS256");
headers.put("typ", "JWT");

return headers;
}

public void validateToken(String token) {
Date claimsExpiration = parseClaimsBody(token).getExpiration();

if (claimsExpiration.before(new Date())) {
throw new RuntimeException("Token Expired");
}
}

public Authentication getAuthentication(String token) {
String ClaimsEmail = parseClaimsBody(token).getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(ClaimsEmail);

return new UsernamePasswordAuthenticationToken(userDetails, " ", userDetails.getAuthorities());
}

private Claims parseClaimsBody(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build().parseClaimsJws(token).getBody();
}

private Claims getClaims(String email) {
Date now = new Date();

return Jwts.claims()
.setSubject(email)
.setIssuer(ISS)
.setExpiration(getExpireTime(now))
.setIssuedAt(now);
}

private Date getExpireTime(Date now) {
return new Date(now.getTime() + 1000 * 60 * EXPIRE);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.devtoon.jtoon.security.jwt.application;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.security.jwt.domain.CustomUserDetails;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Invalid Email"));

return new CustomUserDetails(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.devtoon.jtoon.security.jwt.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.devtoon.jtoon.member.entity.Member;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

private final Member member;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(member.getRole().toString()));
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public String getUsername() {
return member.getEmail();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.devtoon.jtoon.security.presentation;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.devtoon.jtoon.security.application.AuthService;
import com.devtoon.jtoon.security.request.LogInReq;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
public void login(@RequestBody @Valid LogInReq logInReq, HttpServletResponse response) {
String token = authService.login(logInReq);

response.setHeader("Set-Cookie", "Bearer " + token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.devtoon.jtoon.security.request;

import static com.devtoon.jtoon.global.util.RegExp.*;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record LogInReq(
@Pattern(regexp = EMAIL_PATTERN) String email,
@NotBlank String password
) {

}
2 changes: 1 addition & 1 deletion module-application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
spring:
profiles:
include: s3, smtp
include: s3, smtp, iamport, jwt
Loading

0 comments on commit 6b25818

Please sign in to comment.