Login Service[2] 로그인 및 JWT 추가

40562 단어 msamsa

회원가입은 지난 포스트에서 진행했으니 이제 로그인을 만들어보자.

🔨Login Service 로직 만들기

@Getter
@AllArgsConstructor
@ToString
@NoArgsConstructor
public class RequestLogin {
    String userId;
    String pw;
}

RequestLogin을 Vo로 만들었다.

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    //로그인 요청
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            RequestLogin login = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

            //인증정보 생성
            return getAuthenticationManager()
                    .authenticate(
                      new UsernamePasswordAuthenticationToken(
                              login.getUserId(),
                              login.getPw(),
                              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.info("로그인 성공");
    }
}

security의 login 처리를 위해 UsernamePasswordAuthenticationFilter를 상속받아 구현해주었다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
    private final UserService userService;
    private final BCryptPasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        http.authorizeRequests().antMatchers("/join").permitAll();
        http.authorizeRequests().antMatchers("/login").permitAll();

        http.authorizeRequests().antMatchers("/**").permitAll()
        .and().addFilter(getAuthenticationFilter()) //filter 추가
        ;

        http.headers().frameOptions().disable();    //h2 error
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter auth = new AuthenticationFilter();
        auth.setAuthenticationManager(authenticationManager());

        return auth;
    }

    //인증 service 등록
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
    }
}

WebSecurity에 join, login을 제외한 모든 요청에 대해 filter를 적용하도록 하고 인증 service를 등록해준다. userService를 등록하려면 에러가 발생하는데 우리가 만든 userService가 security에서 지원하는 UserDetailsService를 상속받지 않아서이다.

public interface UserService extends UserDetailsService {
    ResponseUser join(RequestUser user);
}

상속시켜주면 이번엔 userService를 상속받은 userServiceImpl에서 에러가 발생한다. method를 구현하면 되는데

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
    private final BCryptPasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    ...

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

loadUserByUsername() 메서드를 구현하면된다. 여기서 id로 user 정보를 불러오고 불러온 정보와 입력된 정보를 비교하는 로직을 진행할거다. 우선 id로 유저 정보를 불러와보자!

@Repository
public interface UserRepository extends CrudRepository<UserEntity, Long> {
    UserEntity findByUserId(String userId);
}

userId값으로 DB를 검색하기 위해 메서드를 추가해주고

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
    private final BCryptPasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    ...

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUserId(userId);
        if(user == null) throw new UsernameNotFoundException("user가 존재하지 않습니다.");

        return new User(user.getUserId(), user.getPw(), true, true, true, true, new ArrayList<>());
    }
}

서버를 실행해서 로그인을 요청해보자!

포스트맨에서 요청하면 정상반환을 받고

정상 로그인 했을 경우 찍어두었던 로그가 찍혀나온다.

아이디나 비밀번호를 틀리게 적으면 401 에러가 나오므로 궁금하면 해봐도 된다.

🔨JWT 추가하기

@Getter
@NoArgsConstructor
@ToString
public class UserVo {
    private String userId;
    private String name;
    private String pw;
    private LocalDateTime createdAt;

    @Builder
    public UserVo(@NonNull String userId, @NonNull String name, @NonNull String pw, @NonNull LocalDateTime createdAt) {
        this.userId = userId;
        this.name = name;
        this.pw = pw;
        this.createdAt = createdAt;
    }
}

먼저 UserVo를 하나 생성하고

@Slf4j
@RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final UserService userService;

    ...
    
    //로그인 성공
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        UserVo user = userService.getUserDetailByUserId(((User) authResult.getPrincipal()).getUsername());
        log.info("로그인 성공 = {}",user.toString());
    }
}

로그인 성공에 UserVo를 불러오도록 userService를 주입해주자.


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
    private final UserService userService;
    private final BCryptPasswordEncoder passwordEncoder;
    private final Environment env;

    ...
    
    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter auth = new AuthenticationFilter(userService, env);	//주입해줘야함!
        auth.setAuthenticationManager(authenticationManager());

        return auth;
    }

    
}

그럼 userService를 filter를 추가하는 부분에도 주입을 해주어야한다.

👉JWT 의존성 추가

implementation 'io.jsonwebtoken:jjwt:0.9.1'

의존성 추가해주고

...

token:
  expiration_time: 86400000 #ms단위
  secret: 비밀 키값

설정 파일에 다음과 같이 설정해준다. token_key값은 자기 비밀키니까 알아서 등록해서 쓰자. 암호화도 가능하지만 어차피 private한 저장소에 넣을거라서 우선은 그냥 써서 넣을거다.


@Slf4j
@RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final UserService userService;
    private final Environment env;

    ...

    //로그인 성공
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        UserVo user = userService.getUserDetailByUserId(((User) authResult.getPrincipal()).getUsername());
        log.info("로그인 성공 = {}",user.toString());
        String token = Jwts.builder()
                .setSubject(user.getUserId())
                .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) //파기일
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))	//암호화 알고리즘과 암호화 키값
                .compact();

        response.setHeader("token", token);
    }
}

성공했을 경우의 로직을 완성해서 response에 header 정보로 token을 추가해준다.

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
    private final BCryptPasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    ...
    
    @Override
    public UserVo getUserDetailByUserId(String userId) {
        UserEntity user = userRepository.findByUserId(userId);

        return UserVo.builder()
                .userId(user.getUserId())
                .pw(user.getPw())
                .name(user.getName())
                .createdAt(user.getCreatedAt())
                .build();
    }
}

그리고 userVo를 반환하는 service를 추가해주자

헤더에 token 정보가 입력되어서 반환되는 것을 확인할 수 있었다.

좋은 웹페이지 즐겨찾기