Merge branch 'jwt' into feature/authentication

# Conflicts:
#	user-service/src/main/java/com/justpickup/userservice/domain/user/entity/User.java
#	user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserService.java
#	user-service/src/main/java/com/justpickup/userservice/domain/user/service/UserServiceImpl.java
This commit is contained in:
bum12ark
2022-02-16 12:00:14 +09:00
25 changed files with 750 additions and 16 deletions

View File

@@ -30,6 +30,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.9.1'
// https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'
// https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
implementation 'javax.xml.bind:jaxb-api:2.3.1'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'

View File

@@ -0,0 +1,74 @@
package com.justpickup.ownerapigatewayservice.filter;
import com.justpickup.ownerapigatewayservice.security.JwtTokenProvider;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) {
super(Config.class);
this.jwtTokenProvider = jwtTokenProvider;
}
static class Config {
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = headers.get(HttpHeaders.AUTHORIZATION).get(0);
// JWT 토큰 판별
String token = authorizationHeader.replace("Bearer", "");
if (jwtTokenProvider.isExpired(token)) {
return onError(exchange, "Access Token is Expired", HttpStatus.UNAUTHORIZED);
}
String subject = jwtTokenProvider.getUserId(token);
if (subject == null) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
ServerHttpRequest request1 = request.mutate()
.header("jwt-sub", subject)
.build();
return chain.filter(exchange.mutate().request(request1).build());
};
}
// Mono(단일 값), Flux(다중 값) -> Spring WebFlux
private Mono<Void> onError(ServerWebExchange exchange, String errorMsg, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(errorMsg);
return response.setComplete();
}
}

View File

@@ -0,0 +1,42 @@
package com.justpickup.ownerapigatewayservice.security;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtTokenProvider {
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String getUserId(String token) {
return getClaimsFromJwtToken(token).getBody().getSubject();
}
public boolean isExpired(String token) {
try {
return getClaimsFromJwtToken(token).getBody().getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
} catch (Exception e) {
return true;
}
}
public Jws<Claims> getClaimsFromJwtToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
}
}

View File

@@ -33,9 +33,36 @@ spring:
- Path=/store-service/**
filters:
- RewritePath=/store-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login
- Method=POST
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/refreshToken
- Method=GET
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/logout
- Method=POST
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
filters:
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- AuthorizationHeaderFilter
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret

View File

@@ -1,8 +1,12 @@
package com.justpickup.userservice;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
@@ -12,4 +16,13 @@ public class UserServiceApplication {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
CommandLineRunner run(UserService userService) {
return args -> {
StoreOwnerDto park = StoreOwnerDto.builder()
.email("test@gmail.com").password("1234").name("Park").phoneNumber("010-1234-5678")
.build();
userService.saveStoreOwner(park);
};
}
}

View File

@@ -0,0 +1,11 @@
package com.justpickup.userservice.domain.jwt.exception;
import com.justpickup.userservice.global.exception.CustomException;
import org.springframework.http.HttpStatus;
public class TokenRefreshException extends CustomException {
public TokenRefreshException(String message) {
super(HttpStatus.FORBIDDEN, message);
}
}

View File

@@ -0,0 +1,8 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
public interface RefreshTokenService {
void updateRefreshToken(Long id, String refreshToken);
JwtTokenDto refreshJwtToken(String accessToken, String refreshToken);
}

View File

@@ -0,0 +1,73 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.jwt.exception.TokenRefreshException;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.domain.user.entity.User;
import com.justpickup.userservice.domain.user.exception.NotExistUserException;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
@Override
public void updateRefreshToken(Long id, String refreshToken) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다."));
user.changeRefreshToken(refreshToken);
}
@Transactional
@Override
public JwtTokenDto refreshJwtToken(String accessToken, String refreshToken) {
String userId = jwtTokenProvider.getUserId(accessToken);
User user = userRepository.findById(Long.valueOf(userId))
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + userId + "는 없는 사용자입니다."));
// refresh token 검증
if (!jwtTokenProvider.validateJwtToken(refreshToken)) {
// 익셉션 발생 - 로그 아웃 후 로그인 페이지로 이동 처리
user.deleteRefreshToken();
throw new TokenRefreshException("Not validate jwt token = " + refreshToken);
}
String userRefreshTokenId = user.getRefreshTokenId();
if (!jwtTokenProvider.equalRefreshTokenId(userRefreshTokenId, refreshToken)) {
// 익셉션 발생 - 로그인 아웃 후 로그인 페이지로 이동 처리
user.deleteRefreshToken();
throw new TokenRefreshException("Not equal jwt token! user = " + userRefreshTokenId +
", refreshToken = " + refreshToken);
}
Authentication authentication = jwtTokenProvider.getAuthentication(user.getEmail());
List<String> roles = authentication.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/refreshToken", roles);
String newRefreshToken = jwtTokenProvider.createJwtRefreshToken();
user.changeRefreshToken(newRefreshToken);
return JwtTokenDto.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.build();
}
}

View File

@@ -0,0 +1,115 @@
package com.justpickup.userservice.domain.jwt.utils;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 java.util.Date;
import java.util.List;
import java.util.UUID;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public Authentication getAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
public String getUserId(String token) {
try {
return getClaimsFromJwtToken(token).getBody().getSubject();
} catch (ExpiredJwtException expiredJwtException) {
return expiredJwtException.getClaims().getSubject();
}
}
private Jws<Claims> getClaimsFromJwtToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
}
public String getRefreshTokenId(String token) {
try {
return getClaimsFromJwtToken(token).getBody().get("value").toString();
} catch (ExpiredJwtException expiredJwtException) {
return expiredJwtException.getClaims().get("value").toString();
}
}
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
return false;
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
return false;
}
}
public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) {
String compareToken = this.getRefreshTokenId(refreshToken);
return refreshTokenId.equals(compareToken);
}
}

View File

@@ -0,0 +1,53 @@
package com.justpickup.userservice.domain.jwt.web;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.global.dto.Result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
@GetMapping("/refreshToken")
public ResponseEntity<Result> refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@RequestHeader("REFRESH-TOKEN") String refreshToken) {
JwtTokenDto jwtTokenDto = refreshTokenServiceImpl.refreshJwtToken(accessToken, refreshToken);
return ResponseEntity.ok(Result.createSuccessResult(new RefreshTokenResponse(jwtTokenDto)));
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class RefreshTokenResponse {
private String accessToken;
private String refreshToken;
public RefreshTokenResponse(JwtTokenDto jwtTokenDto) {
this.accessToken = jwtTokenDto.getAccessToken();
this.refreshToken = jwtTokenDto.getRefreshToken();
}
}
@PostMapping("/logout")
public ResponseEntity<Result> logout(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@RequestHeader("REFRESH-TOKEN") String refreshToken) {
log.info("########### logout!");
// TODO: 2022/02/16 logout 구현 필요
return ResponseEntity.ok(Result.createSuccessResult("success"));
}
}

View File

@@ -16,8 +16,9 @@ public class CustomerDto extends UserDto {
}
@Builder
public CustomerDto(Long id, String password, String name, String phoneNumber) {
super(id, password, name, phoneNumber);
public CustomerDto(Long id, String email, String password, String name,
String phoneNumber, String dtype, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
}
}

View File

@@ -0,0 +1,16 @@
package com.justpickup.userservice.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class JwtTokenDto {
private String accessToken;
private String refreshToken;
@Builder
public JwtTokenDto(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}

View File

@@ -0,0 +1,16 @@
package com.justpickup.userservice.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class StoreOwnerDto extends UserDto {
private String businessNumber;
@Builder
public StoreOwnerDto(Long id, String email, String password, String name,
String phoneNumber, String dtype, String businessNumber, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
this.businessNumber = businessNumber;
}
}

View File

@@ -1,15 +1,17 @@
package com.justpickup.userservice.domain.user.dto;
import com.justpickup.userservice.domain.user.entity.Customer;
import lombok.Builder;
import lombok.Getter;
@Getter
public class UserDto {
public abstract class UserDto {
private Long id;
private String email;
private String password;
private String name;
private String phoneNumber;
private String dtype;
private String refreshTokenId;
// == 생성 메소드 == //
public UserDto(Customer customer) {
@@ -19,10 +21,14 @@ public class UserDto {
this.phoneNumber = customer.getPhoneNumber();
}
public UserDto(Long id, String password, String name, String phoneNumber) {
public UserDto(Long id, String email, String password, String name, String phoneNumber,
String dtype, String refreshTokenId) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
this.phoneNumber = phoneNumber;
this.dtype = dtype;
this.refreshTokenId = refreshTokenId;
}
}

View File

@@ -1,5 +1,6 @@
package com.justpickup.userservice.domain.user.entity;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -12,4 +13,10 @@ import javax.persistence.Table;
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StoreOwner extends User {
private String businessNumber;
public StoreOwner(String email, String password, String name, String phoneNumber,
String businessNumber, String refreshTokenId) {
super(email, password, name, phoneNumber, refreshTokenId);
this.businessNumber = businessNumber;
}
}

View File

@@ -12,7 +12,7 @@ import javax.persistence.*;
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class User extends BaseEntity {
public class User extends BaseEntity {
@Id @GeneratedValue
@Column(name = "user_id")
@@ -26,6 +26,8 @@ public abstract class User extends BaseEntity {
private String phoneNumber;
private String refreshTokenId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@@ -40,4 +42,20 @@ public abstract class User extends BaseEntity {
this.phoneNumber = phoneNumber;
this.role = role;
}
public User(String email, String password, String name, String phoneNumber, String refreshTokenId) {
this.email = email;
this.password = password;
this.name = name;
this.phoneNumber = phoneNumber;
this.refreshTokenId = refreshTokenId;
}
public void changeRefreshToken(String refreshToken) {
this.refreshTokenId = refreshToken;
}
public void deleteRefreshToken() {
this.refreshTokenId = null;
}
}

View File

@@ -0,0 +1,10 @@
package com.justpickup.userservice.domain.user.repository;
import com.justpickup.userservice.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String username);
}

View File

@@ -5,8 +5,10 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.entity.StoreOwner;
public interface UserService extends OAuth2UserService<OAuth2UserRequest, OAuth2User> {
CustomerDto findCustomerByUserId(Long userId);
StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto);
}

View File

@@ -2,11 +2,17 @@ package com.justpickup.userservice.domain.user.service;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.justpickup.userservice.domain.user.dto.CustomerDto;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto;
import com.justpickup.userservice.domain.user.dto.OAuthAttributeDto;
import com.justpickup.userservice.domain.user.entity.Customer;
import com.justpickup.userservice.domain.user.entity.StoreOwner;
import com.justpickup.userservice.domain.user.entity.User;
import com.justpickup.userservice.domain.user.exception.NotExistUserException;
import com.justpickup.userservice.domain.user.repository.CustomerRepository;
import lombok.*;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -16,6 +22,13 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.core.env.Environment;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -23,16 +36,36 @@ import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Collections;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Collection;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class UserServiceImpl implements UserService {
public class UserServiceImpl implements UserService, UserDetailsService {
private final CustomerRepository customerRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final HttpServletResponse response;
private final Environment env;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found in the database"));
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getDtype()));
return new org.springframework.security.core.userdetails.User(user.getId().toString(), user.getPassword(), authorities);
}
@Override
public CustomerDto findCustomerByUserId(Long userId) {
Customer customer = customerRepository.findById(userId)
@@ -41,6 +74,17 @@ public class UserServiceImpl implements UserService {
return new CustomerDto(customer);
}
@Override
@Transactional
public StoreOwner saveStoreOwner(StoreOwnerDto storeOwnerDto) {
String encode = passwordEncoder.encode(storeOwnerDto.getPassword());
StoreOwner storeOwner = new StoreOwner(storeOwnerDto.getEmail(), encode, storeOwnerDto.getName(),
storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber(), storeOwnerDto.getRefreshTokenId());
return userRepository.save(storeOwner);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -82,7 +126,7 @@ public class UserServiceImpl implements UserService {
Customer customer = customerRepository.save(
customerRepository.findByEmail(attributeDto.getEmail())
.orElse(attributeDto.toEntity(attributeDto))
.orElse(attributeDto.toEntity(attributeDto))
);
// TODO: 2022/02/16 Response에 token 담아 보내기
@@ -104,9 +148,4 @@ public class UserServiceImpl implements UserService {
this.email = user.getEmail();
}
}
}

View File

@@ -0,0 +1,15 @@
package com.justpickup.userservice.global.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,10 @@
package com.justpickup.userservice.global.dto;
import lombok.Data;
@Data
public class LoginRequest {
private String name;
private String email;
private String password;
}

View File

@@ -0,0 +1,28 @@
package com.justpickup.userservice.global.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class HeaderAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServletPath().equals("/login")) {
filterChain.doFilter(request, response);
return;
}
String email = request.getHeader("jwt-sub");
log.info("email jwt-sub = {}", email);
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,88 @@
package com.justpickup.userservice.global.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import com.justpickup.userservice.global.dto.LoginRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RequiredArgsConstructor
@Slf4j
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
// login 리퀘스트 패스로 오는 요청을 판단
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Authentication authentication;
try {
LoginRequest credential = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(credential.getEmail(), credential.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return authentication;
}
// 로그인 성공 이후 토큰 생성
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal();
List<String> roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String userId = user.getUsername();
String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles);
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken));
Map<String, String> tokens = Map.of(
"access_token", accessToken,
"refresh_token", refreshToken
);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
@Override
protected void unsuccessfulAuthentication
(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
log.warn("로그인 실패!!");
}
}

View File

@@ -0,0 +1,55 @@
package com.justpickup.userservice.global.security;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
LoginAuthenticationFilter loginAuthenticationFilter =
new LoginAuthenticationFilter(authenticationManagerBean(), jwtTokenProvider, refreshTokenServiceImpl);
loginAuthenticationFilter.setFilterProcessesUrl("/login");
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().anyRequest().permitAll();
http.logout()
.logoutUrl("/logout")
.deleteCookies("");
http.addFilter(loginAuthenticationFilter);
http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

View File

@@ -43,4 +43,9 @@ logging:
# jpa query, parameter 로그 (p6spy)
decorator.datasource.p6spy:
enable-logging: true
enable-logging: true
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret