MYSQL + JPA + JWT + Spring Security 를 사용해서 로그인 구현하기 위해 JWT구현 로직을 공부했다
- JWT가 유효한 토큰인지 인증하기 위한 Filter
@RequiredArgsConstructor
//자동으로 Constructor를 만들어줌
public class JwtAutenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
// Request로 들어오는 Jwt Token의 유효성을 검증하는 filter를 filterChain에 등록
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException,ServletException{
// 헤더에서 JWT를 받아옴
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
//유효한 토큰인지 확인
if(token != null && jwtTokenProvider.validateToken(token)) {
//토큰이 유효하면 토큰으로부터 유저 정보를 받아옴
Authentication authentication = jwtTokenProvider.getAuthentication(token);
//SecurityContext에 Authentication 객체를 저장함
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
- JWT Token을 생성, 인증, 권한 부여, 유효성 검사, PK 추출 기능을 제공하는 클래스
@RequiredArgsConstructor
//자동으로 Constructor를 생성
@Component
//빈 직접 등록
public class JwtTokenProvider {
private String secretKey = "mobee";
//토큰 유효시간 30분
private long tokenValidTime = 30 * 60 * 1000L;
private final UserDetailsService userDetatilsService;
// 객체 초기화, secretKey를 Base64로 인코딩
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
//JWT 토큰 생성
public String createToken(String nickname, String profileImgUrl, List<String> roles){
Claims claims = Jwts.claims().setSubject(nickname);
//JWT payload에 저장되는 정보단위
claims.put("nickname", nickname);
claims.put("profileImgUrl", StringUtils.hasText(profileImgUrl) ? prifileImgUrl : "");
claims.put("roles", roles);
// 정보는 key / value 쌍으로 저장된다
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //데이터 정보저장
.setIssuedAt(now) //토큰 발행 시간
.setExpiration(new Date(now.getTime() + tokenVaildTime)) // setExpire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과 signature 에 들어갈 secret값 세팅
.compact(); //토큰 생성
}
//인증 성공시 SecurityContextHolder에 저장할 Authentication 객체 생성
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) {
return request.getHeader("Authorization");
}
// 토큰의 유효성 + 만료일자 확인
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;
}
}
}
- Spring Security에서 사용자의 정보를 담는 인터페이스
@RequiredArgsConstructor
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private final AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String nickname) throws UsernameNotFoundException {
Account account = accountRepository.findByNickname(nickname)
.orElseThrow(() -> new UsernameNotFoundException("Can't find " + nickname));
return new UserDetailsImpl(account);
}
- DB에서 유저 정보를 직접 가져오는 인터페이스
public class UserDetailsImpl implements UserDetails {
private final Account account;
public UserDetailsImpl(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return account.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.getNickname();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
- 스프링 시큐리티의 웹 보안 기능 초기화 및 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함 Spring Security Filter Chain 을 사용한다는 것
//@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@Bean // 패스워드 인코딩
public BCryptPasswordEncoder encodePassword(){
return new BCryptPasswordEncoder();
}
/* AuthenticationManager 를 이용하여, 원하는 시점에 로그인이 될 수 있도록 바꿔줌,
먼저, AuthenticationManager 를 외부에서 사용 하기 위해, AuthenticationManagerBean 을 이용하여
Sprint Securtiy 밖으로 AuthenticationManager 빼 내야 한다.*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(ImmutableList.of("*")); // 스프링부트 2.4부터 변경?
configuration.setAllowedMethods(ImmutableList.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "TOKEN_ID", "X-Requested-With", "Content-Type", "Content-Length", "Cache-Control"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(corsConfigurationSource());
// http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
http
.httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
.headers().frameOptions().disable().and()
.csrf().disable() // csrf 보안 토큰 disable처리.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
.and()
.authorizeRequests() // 요청에 대한 사용권한 체크
.antMatchers("/h2-console/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/posts/**").authenticated()
.antMatchers(HttpMethod.PUT, "/api/posts/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/posts/**").authenticated()
.anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
.and()
.addFilterBefore(new JwtAutenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}
'프레임워크 > 스프링' 카테고리의 다른 글
[Spring] 크롤링하여 json 형태로 저장 (0) | 2022.04.29 |
---|---|
[Spring] Spring Boot, React, Redux 회원가입 구현 (백엔드) (0) | 2022.04.26 |
[Spring Boot] JSOUP 과 MongoDB를 사용해서 실시간 멜론 차트 가져오기 (0) | 2022.03.24 |
[Spring Boot] MariaDB, maven을 활용한 게시판 만들기 (3) (0) | 2022.03.15 |
[Spring Boot] MariaDB, maven을 활용한 게시판 만들기 (2) (0) | 2022.03.15 |
댓글