Spring Cloud(MSA)

Users Microservice - 인증과 권한 기능 개요

lby132 2022. 9. 21. 18:00

Users라는 사용자 정보가 담긴 마이크로서비스를 추가했다.

사용자에 대한 인증과 권한 기능을 구현해 본다.

 

사용자에게 입력받을 객체이다.

@Data
public class RequestLogin {
    @NotNull(message = "Email cannot be null")
    @Size(min = 2, message = "Email not be less then two characters")
    @Email
    private String email;
    @NotNull(message = "Password cannot be null")
    @Size(min = 8, message = "Password must be equals or greater then 8 characters")
    private String password;
}

사용자가 입력한 아이디와 패스워드를 필터한다.

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

            // 사용자가 입력한 아이디와 패스워드를 UsernamePasswordAuthenticationToken으로 바꾼걸 AuthenticationManager(인증처리해주는 매니저)에 인증작업을 요청하면 아이디와 패스워드를 비교해준다.
            return getAuthenticationManager().authenticate(
                    //사용자가 입력한 아이디와 패스워드를 스프링 시큐리티에서 사용할 수 있는 형태로 변환하기 위해서 UsernamePasswordAuthenticationToken으로 바꿔 줘야한다.
                    new UsernamePasswordAuthenticationToken(creds.getEmail(), creds.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 {

        log.debug( ((User)authResult.getPrincipal()).getUsername() );   // 로그인한 사용자 이름 반환

        String username = ((User) authResult.getPrincipal()).getUsername();
        UserDto userDetails = userService.getUserDetailsByEmail(username);

        String token = Jwts.builder()       // jsonWebToken인 jjwt를 주입받으면 만들수있다.
                .setSubject(userDetails.getUserId())
                //setExpiration에는 토큰 만료기간을 정해줘야하는데 현재시간과 설정파일에 설정한 시간 token.expiration_time=8640000 을 더해줘서 토큰 만료일을 정한다.
                .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) // 문자열이므로 숫자로 변환했다.
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))    // 토큰을 생성할때 설정파일에서 설정한 token.secret=user_token 이 키를 가지고 생성한다.
                .compact();

        response.addHeader("token", token);     // 헤더에 토큰을 반환한다.
        response.addHeader("userId", userDetails.getUserId());      // 데이터베이스에서 가져온 유저 아이디

    }
}

스프링 시큐리티 설정

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();
//        http.authorizeRequests().antMatchers("/users/**").permitAll(); // /users로 들어온 모든 작업은 통과시킨다.
//
//        http.headers().frameOptions().disable();    // 프레임별로 구분된거 무시됨
//    }

    private UserService userService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;    // 빈으로 등록한 패스워드를 암호화 해주는 BCryptPasswordEncoder를 주입한다.
    private Environment env;        // yml에 있는 설정들을 가져다가 쓸수 있다.

    @Autowired
    public WebSecurity(UserService userService, BCryptPasswordEncoder bCryptPasswordEncoder, Environment env) {
        this.userService = userService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.env = env;
    }

    @Override //HttpSecurity를 매개변수로 받는 configure메서드는 권한에 관련된 작업이다.
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests().antMatchers("/error/**").permitAll()     //에러 요청을 처리해야해서 에러에 대한 부분만 통과
                .antMatchers("/**")         // 모든 작업에 통과시키지 않는다.
                .access("hasIpAddress('"+"내 아이피주소"+"')")  // 내 아이피 주소로온것만 허용
                .and()
                .addFilter(getAuthenticationFilter());  // 여기에 통과된 데이터만 권한을 부여하고 작업을 진행을 한다.

        http.headers().frameOptions().disable();    // 프레임별로 구분된거 무시됨
    }
    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        authenticationFilter.setAuthenticationManager(authenticationManager());

        return authenticationFilter;
    }

    // select pwd from users where email = ?
    // db_pwd(encrypted) == input_pwd(encrypted)
    @Override//AuthenticationManagerBuilder를 매개변수로 받는 configure메서드는 인증에 관련된 작업이다.
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
}

주입 받은 객체를 살펴보면 UserService를 주입받은 이유는 맨 아래에 있는 configure메소드는 인증에 관련된 작업을 하기 위한 메서드인데 userDetailsService()라는 메소드에서 유저 정보를 받기때문인데 다시 UserService를 보면

public interface UserService extends UserDetailsService {

    UserDto createUser(UserDto userDto);

    UserDto getUserByUserId(String userId);

    Iterable<UserEntity> getUserByAll();
}

userDetailsService()라는 메소드를 사용하기 위해서는 UserService에서 UserDetailsService를 상속 받아야한다.

그리고 UserServiceImpl에서 구현한 코드이다.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 사용자 이름으로 이메일 정보를 가져와서
    UserEntity userEntity = userRepository.findByEmail(username);

    // 사용자 정보가 없으면 에러를 발생시킨다.
    if (userEntity == null) {
        throw new UsernameNotFoundException(username);
    }

    // 사용자 정보가 있으면 security에 있는 User()에 파라미터로 사용자 정보를 넣는다. 마지막 파라미터는 권한을 추가해서 넣어주면 되는데 권한을 추가한게 없어서 일단 비어있는 new ArrayList<>()로 반환.
    return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(), true, true,
            true, true, new ArrayList<>());
}

그리고 WebSecurity클래스에 주입된  BCryptPasswordEncoder를 보면

다시 configure메소드에 userDetailsService()말고 passwordEncoder(bCryptPasswordEncoder) 가 있는데

사용자 정보에 비밀번호를 입력받으면 암호화를 시켜주기 위해

@Bean   // 사용자가 입력한 패스워드를 encrypted password로 변환해준다.
public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

이렇게 빈으로 등록을 받았었다. 암호화로 변환된 비밀번호를 passwordEncoder()에 넣어주기 위해 주입을 받았다.