[Spring Security] JWT 로그인 구현하기
이멀젼씨
·2021. 6. 25. 19:04
목적
세션을 사용하지 않고 로그인을 구현하기 위함
목차
- 의존성 추가
- UserDetails 생성
- UserRepository 추가
- UserDetailsService 생성
- JWT유틸 클래스 추가
- JWT필터 추가
- 스프링 시큐리티 설정
- 스프링 설정
- 컨트롤러 설정
Refresh Token을 활용한 JWT로그인의 구현은 아래 링크를 참조하면 되겠다.
1. 의존성 추가
jwt 의존성을 추가해준다
아래는 gradle 기준이다.
implementation 'io.jsonwebtoken:jjwt:0.9.1'
2. UserDetails 생성
스프링 시큐리티에서 관리하는 UserDetails타입의 객체를 생성해야한다.
기본 필드 및 메서드 외 필요한 것들은 추가해서 사용하면 된다.
public class User implements UserDetails{
@Id
@GeneratedValue
private Long id;
private String email;
private String password;
private String phoneNumber;
private String name;
@Enumerated(EnumType.STRING)
private Gender gender;
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Thumbnail> thumbnailList = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public User updateInfo(UserDto userDto){
this.email = userDto.getEmail();
this.password = userDto.getPassword();
this.phoneNumber = userDto.getPhoneNumber();
this.name = userDto.getName();
this.gender = userDto.getGender();
return this;
}
}
3. UserRepository 추가
스프링 시큐티리에서 JPA를 사용하기 때문에 User타입을 다루는 레포지토리를 추가해야한다.
findByEmail은 이메일로 유저를 찾기 위해 선언하였다.
기본 ID로 찾을거면 굳이 선언하지 않아도 된다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
4. UserDetailsService 생성
스프링시큐리티에서 유저를 찾는 메소드를 제공하는 UserDetailsService를 추가한다.
UserDetailsService의 loadUserByUsername메소드를 오버라이딩 하여 유저를 찾는 방법을 직접 지정한다.
반환타입은 UserDetails타입이어야한다.
public class CustomUserDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
}
}
5. JWT유틸 클래스 추가
JWT을 생성, 검증, 정보추출 해주는 클래스이다.
JWT필터 안에서만 사용할 수 있으면 된다.
public class JwtAuthenticationProvider {
private String secretKey = "secret";
private long tokenValidTime = 1000L * 60 * 60;
@Autowired
private UserDetailsService userDetailsService;
// JWT 토큰 생성
public String createToken(String userPk, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
String token = null;
Cookie cookie = WebUtils.getCookie(request, "X-AUTH-TOKEN");
if(cookie != null) token = cookie.getValue();
return token;
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
6. JWT필터 추가
인증에 성공하면 authentication객체를 Context안에 넣는다.
인증에 실패하면 아무런 과정 없이 다음 필터로 넘어간다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtAuthenticationProvider jwtAuthenticationProvider;
public JwtAuthenticationFilter(JwtAuthenticationProvider provider) {
jwtAuthenticationProvider = provider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtAuthenticationProvider.resolveToken(request);
if(token != null && jwtAuthenticationProvider.validateToken(token)){
Authentication authentication = jwtAuthenticationProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
7. 스프링 시큐리티 설정
JWT필터를 스프링 시큐리티가 관리하려면 설정에서 추가해줘야한다.
필터는 new를 사용해서 객체를 생성하여 등록해주어야 한다.
필터에 @Component나 @Bean을 붙이게 되면 new로 선언해준것 외에도 한번 더 필터로 등록되기 때문이다.
쿠키에 있는 토큰을 전달할 것이기 때문에 CORS설정 또한 해주어야한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.cors().and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtAuthenticationProvider), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
8. 스프링 설정
스프링 시큐리티에서 스프링의 CORS설정을 가져다 사용할 수 있도록 설정해준다.
클라이언트의 쿠키를 전달하고 받을 것이기 때문에 allowCredentials를 true로 설정한다.
클라이언트의 쿠키에 전달한 헤더를 exposedHeader안에 넣어주어야 한다.
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.exposedHeaders("X-AUTH-TOKEN")
.allowCredentials(true)
.allowedOrigins("http://localhost:3000");
}
}
9. 컨트롤러 설정
로그인 시 사용자의 정보가 일치하면 토큰을 만들어 쿠키에 추가해준다.
로그아웃 요청시엔 토큰정보를 갖고 있는 헤더를 지워준다.
/info 는 클라이언트에서 새로고침 시 요청이 들어오는 경로다. 클라이언트의 토큰이 유효하면 정보를 꺼내서 주고, 아니면 아무것도 주지 않는다.
public class UserApi {
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
@PostMapping("/join")
public void join(@RequestBody UserDto user){
userRepository.save(User.builder()
.email(user.getEmail())
.password(passwordEncoder.encode(user.getPassword()))
.name(user.getName())
.phoneNumber(user.getPhoneNumber())
.gender(user.getGender())
.roles(Collections.singletonList("ROLE_USER"))
.build());
}
@PostMapping("/login")
public UserDto login(@RequestBody UserDto user, HttpServletResponse response) {
User member = userRepository.findByEmail(user.getEmail())
.orElseThrow(() -> new IllegalArgumentException("가입되지 않은 E-MAIL 입니다."));
if (!passwordEncoder.matches(user.getPassword(), member.getPassword())) {
throw new IllegalArgumentException("잘못된 비밀번호입니다.");
}
String token = jwtAuthenticationProvider.createToken(member.getUsername(), member.getRoles());
response.setHeader("X-AUTH-TOKEN", token);
Cookie cookie = new Cookie("X-AUTH-TOKEN", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
return new UserDto(member);
}
@PostMapping("/logout")
public void logout(HttpServletResponse response){
Cookie cookie = new Cookie("X-AUTH-TOKEN", null);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
}
@GetMapping("/info")
public UserDto getInfo(){
Object details = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(details != null && !(details instanceof String)) return new UserDto((User) details);
return null;
}
}
'백엔드 > Spring Security' 카테고리의 다른 글
[Spring Security] Role Hierarchy 설정하기 (0) | 2021.06.24 |
---|---|
[Spring Security] DelegatingFilterProxy의 동작과정 (0) | 2021.06.12 |
[Spring Security] 커스텀 필터 생성 시 'authenticationmanager must be specified' (0) | 2021.05.02 |
[Spring Security] Spring Security 동작원리 (0) | 2021.04.18 |