회원가입은 지난 포스트에서 진행했으니 이제 로그인을 만들어보자.
@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 에러가 나오므로 궁금하면 해봐도 된다.
@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를 추가하는 부분에도 주입을 해주어야한다.
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 정보가 입력되어서 반환되는 것을 확인할 수 있었다.