티스토리 뷰

MSA

Section 6: Users Microservice-2

✨✨✨✨✨✨✨ 2023. 6. 7. 17:07
반응형

Users Microservice - Login

JWT (Json Web Token)

API Gateway service - AuthoriztionHeaderFilter

UsersMicroservice - AuthenticationFilter

  1. 이전 만들었던 회원가입을 진행한다.

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
링크
«   2024/10   »
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
글 보관함