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()에 넣어주기 위해 주입을 받았다.