Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JT-29] JWT 로그인 기능 구현 #9

Merged
merged 7 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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