티스토리 뷰
Users Microservice - Login
JWT (Json Web Token)
API Gateway service - AuthoriztionHeaderFilter
UsersMicroservice - AuthenticationFilter
- 이전 만들었던 회원가입을 진행한다.
2. /user-service/login 으로 POST 데이터를 날려본다
아래와 같이 200 OK 떨어짐을 확인할 수 있다.
그리고 이메일을 혹은 비밀번호를 틀리게하여 로그인하면 아래와같이 401 - Unauthorized 가 표시된다
그런데 특이한점은
UserController의 경우 /login 을 만들지 않았다.
@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class UserController {
private final Greeting greeting;
private final UserService userService;
@GetMapping("/health_check")
public String status(){
return "It's Working in User Service";
}
@GetMapping("/greeting")
public String getGreeting(){
return greeting.getMessage();
}
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody RequestUser requestUser){
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = modelMapper.map(requestUser, UserDto.class);
userDto = userService.createUser(userDto);
ResponseUser userResponse = modelMapper.map(userDto, ResponseUser.class);
return new ResponseEntity<>(userResponse, HttpStatus.CREATED);
}
@GetMapping("/users")
public ResponseEntity<?> getUsers(){
List<UserDto> users = userService.getUserByAll();
ArrayList<ResponseUser> responseUsers = new ArrayList<>();
users.forEach(u -> responseUsers.add(new ModelMapper().map(u, ResponseUser.class)));
return new ResponseEntity<>(responseUsers, HttpStatus.OK);
}
@GetMapping("/users/{userId}")
public ResponseEntity<?> getUserByUserId(@PathVariable String userId){
UserDto user = userService.getUserByUserId(userId);
ResponseUser responseUser = new ModelMapper().map(user, ResponseUser.class);
return new ResponseEntity<>(responseUser, HttpStatus.OK);
}
}
user-service WebSecurity.java` 파일을 보면 다음과 같다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final Environment environment;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// http.authorizeRequests().antMatchers("/users/*").permitAll();
http.authorizeRequests().antMatchers("/**") // 모든 URL 패턴 허용
.hasIpAddress("192.168.0.15")
.and()
.addFilter(getAuthenticationFilter()); // 해당 필터 통과한 데이터만 작업처리 : AuthenticationFilter(정의)
http.headers().frameOptions().disable();
}
// 2. AuthenticationFilter extends UsernamePasswordAuthenticationFilter 하여 가지고온다.
// 3. 정의한 AuthenticationFilter와 아래 configure(Manager)를 등록한다
private AuthenticationFilter getAuthenticationFilter() throws Exception{
AuthenticationFilter authenticationFilter = new AuthenticationFilter(userService, environment);
authenticationFilter.setAuthenticationManager(authenticationManager());
return authenticationFilter;
}
// select pwd from users where email = ?
// db_pwd(encrypted) == input_pwd(encrypted)
// 1. authenticationManager를 Build해놓는다
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
}
또한 AuthenticationFilter에서 아래 보면 /POST 로 /login을 제공해줌을 알 수 있다.
그리고 configure 메소드를 확인해보면 아래와 같은 문장이 있다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
이는 userService를 extends UserDetailsService 하였고
public interface UserService extends UserDetailsService {
UserDto createUser(UserDto userDto);
UserDto getUserByUserId(String userId);
List<UserDto> getUserByAll();
}
UserServiceImpl은 loadUserByUsername 를 Override 함으로써 /login 시도시에
findByEmail을 찾아서 전달해주었고 해당 비밀번호도 함께 확인하여 값을 넘겨준다
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(username);
if(userEntity == null)
throw new UsernameNotFoundException(username);
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true, true,
new ArrayList<>());
}
그렇다면 /login 을 날려보면
아래 부분에 DEBUG가 걸림을 알 수 있다.
UsernamePasswordAuthenticationFilter를 상속받은 AuthenticationFilter 이기에
이는 WebSecurity에서 getAuthenticationFilter() 에 해당 필터를 넘겨야만 한다는 조건을 설정해두었기 때문이다.
@Slf4j
@RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final UserService userService;
private final Environment env;
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin requestLogin = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestLogin.getEmail(), requestLogin.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 로그인 성공 시 JWT 토큰 발행
String username = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetail = userService.getUserDetailByEmail(username);
LocalDateTime localDateTime = LocalDateTime.now().plusHours(Long.parseLong(env.getProperty("token.expiration_hours")));
Date date = Timestamp.valueOf(localDateTime);
String token = Jwts.builder()
.setSubject(userDetail.getUserId())
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetail.getUserId());
}
}
위 필터를 지나 UserServiceImpl에서 유저정보(이메일, 비밀번호)를 확인한다.
완성코드는 다음와 같다.
apigateway-service - application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: <http://localhost:8761/eureka>
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login # Spring Security가 정의해준 메소드
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\\{segment}
- AuthorizationHeaderFilter
token:
secret:
- apigateway - 회원가입/ 로그인 제외한 나머지 요청을 AuthorizationHeaderFilter 를 거치게된다. 이는 jwt로 발급된 token을 확인하고 유효한 토큰인지 검사하게된다.
apigateway-service AuthorizationHeaderFilter.java
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
@Autowired
private Environment env;
public AuthorizationHeaderFilter(){
super(Config.class);
}
public static class Config{
}
// login -> token ->user (with token) -> header(include token)
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorization = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
String token = authorization.replace("Bearer ", "");
if(!isJwtValid(token)) {
return onError(exchange, "No token is not valid", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
}));
};
}
private boolean isJwtValid(String token) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser()
.setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(token).getBody()
.getSubject();
} catch (Exception e) {
returnValue = false;
}
if(subject == null || subject.isEmpty() ){
returnValue = false;
}
return returnValue;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
user-service - UserController.java
~~~ 생략 ~~~
@RequestMapping("/")
~~~ 생략 ~~~
- apigateway-service에서 RewritePath=/user-service/(?<segment>.*), /$\\{segment} 을 넣어줌으로써 요청 UserController.java에서 RequestMapping(”/user-service”) 를 생략가능
user-service - UserService.java
public interface UserService extends UserDetailsService {
UserDto createUser(UserDto userDto);
UserDto getUserByUserId(String userId);
List<UserDto> getUserByAll();
UserDto getUserDetailByEmail(String username);
}
- UserDetailsService 상속
user-service - UserServiceImpl.java
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(username);
if(userEntity == null)
throw new UsernameNotFoundException(username);
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true, true,
new ArrayList<>());
}
@Override
public UserDto getUserDetailByEmail(String email) {
UserEntity userEntity = userRepository.findByEmail(email);
if(userEntity == null)
throw new UsernameNotFoundException("User not found");
return new ModelMapper().map(userEntity, UserDto.class);
}
~~~ 생략 ~~~
- loadUserByUsername 를 Override 해줌으로써 로그인 인증처리
user-service - WebSecurity.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final Environment environment;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// http.authorizeRequests().antMatchers("/users/*").permitAll();
http.authorizeRequests().antMatchers("/**") // 모든 URL 패턴 허용
.hasIpAddress("192.168.0.15")
.and()
.addFilter(getAuthenticationFilter()); // 해당 필터 통과한 데이터만 작업처리 : AuthenticationFilter(정의)
// 해당 부분은 attemptAuthentication인증 시도와
// successfulAuthentication 성공 후 처리를 정의함
http.headers().frameOptions().disable();
}
// 2. AuthenticationFilter extends UsernamePasswordAuthenticationFilter 하여 가지고온다.
// 3. 정의한 AuthenticationFilter와 아래 configure(Manager)를 등록한다
private AuthenticationFilter getAuthenticationFilter() throws Exception{
AuthenticationFilter authenticationFilter = new AuthenticationFilter(userService, environment);
authenticationFilter.setAuthenticationManager(authenticationManager());
return authenticationFilter;
}
// select pwd from users where email = ?
// db_pwd(encrypted) == input_pwd(encrypted)
// 1. authenticationManager를 Build해놓는다
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
}
- 위 configure 에서 userService를 등록하여 로그인 처리 방법 정의
- 아래 configure 에서 INPUT 관련 filter 정의
user-service - AuthenticationFilter.java
@Slf4j
@RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final UserService userService;
private final Environment env;
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin requestLogin = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestLogin.getEmail(), requestLogin.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 로그인 성공 시 JWT 토큰 발행
String username = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetail = userService.getUserDetailByEmail(username);
LocalDateTime localDateTime = LocalDateTime.now().plusHours(Long.parseLong(env.getProperty("token.expiration_hours")));
Date date = Timestamp.valueOf(localDateTime);
String token = Jwts.builder()
.setSubject(userDetail.getUserId())
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetail.getUserId());
}
}
- attemptAuthentication 로그인 시도 시 AuthenticationFilter 내용 정의
- successfulAuthentication 이 후, 로그인 성공 시 해당 메소드 실행
'MSA' 카테고리의 다른 글
Section 8: Spring Cloud Bus (0) | 2023.06.07 |
---|---|
Section 7: Configuration Service (0) | 2023.06.07 |
Section 4: Users Microservice -1 (0) | 2023.06.07 |
Section 3: E-commerce 애플리케이션 (0) | 2023.06.07 |
Section 2: API Gateway Service (0) | 2023.06.07 |
- Total
- Today
- Yesterday
- JPA
- AnnotationConfigApplicationContext
- springboot
- 중간연산
- 차이
- Vue
- map
- nginx
- MAC
- BeanFactory
- ngnix
- Intellij
- stream
- Vuex
- docker
- install
- 스트림
- mvn
- 람다
- elasticsearch
- ApplicationContext
- container
- webpack
- java
- 영속성 컨텍스트
- lambda
- NPM
- 자바8
- vscode
- 최종연산
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |