What to do?
인증기능
JWT를 사용한 인증기능 구현
UserEntity
- UserDetail을 implement하고, 아래의 메써드들을 오버라이딩
@Entity
public class UserEntity implements UserDetails {
...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Set.of(new SimpleGrantedAuthority(role.name()));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
(지난번 포스팅과 코드는 동일)
application.yaml 파일
- JWT 생성에 필요한 비밀키를 환경변수로 등록

- JWT 생성에 필요한 설정 추가
- secret-key : 암호화에 필요한 비밀키
- duration : JWT 유효기간 (단위 : 1/1000초)
jwt:
secret-key: ${DEV_JWT_SECRET_KEY}
duration: 6048000000 # 7일

JWT Filter
- doFiterInternal : 인증기능 처리
- 헤더 체크
- null이 아닌지
- Bearer XXXX 와 같은 문자열이 맞는지
- Claims 추출
- 유효기간 체크
- 유효한 토큰인 경우 SecurityContextHolder에 인증정보 담음
- 헤더 체크
@Slf4j
@RequiredArgsConstructor
public class JwtUtil extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
/**
* JWT 생성
* @param username 인코딩할 문자열에 유저명 사용
* @param secretKey 보안키 값
* @param duration 토큰 유효시간 (Milli Second)
* @return jwt 토큰값
*/
public static String generateToken(String username, String secretKey, long duration){
Claims claims = Jwts.claims();
claims.put("username", username);
long currentTimeMillis = System.currentTimeMillis();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(currentTimeMillis))
.setExpiration(new Date(currentTimeMillis+duration))
.signWith(
Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)),
SignatureAlgorithm.HS256
).compact();
}
/**
* JWT에서 claims 추출
* @param jwt 토큰
* @param secretKey 비밀키
* @return Claims
*/
public static Claims extractClaimsFromJwt(String jwt, String secretKey){
Key signingKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더가 null이 아닌지 체크
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
log.error("Header is null...");
filterChain.doFilter(request, response);
return;
// JWT토큰이 Bearer로 시작하는 문자열이 맞는지 확인
} else if (!header.startsWith("Bearer ")) {
log.error("Authorization Header does not start with Bearer...");
filterChain.doFilter(request, response);
return;
}
// 인증토큰에서 JWT 분리
final String jwt = header.split(" ")[1].trim();
Claims claims = extractClaimsFromJwt(jwt, secretKey);
// 토큰 유효기간이 지났는지 체크
if (claims.getExpiration().before(new Date())){
log.error("Token is expired...");
filterChain.doFilter(request, response);
return;
}
try {
// JWT에서 유저명 추출
String usernameFromJwt = claims.get("username", String.class);
UserEntity user = userService.findByUsernameOrElseThrow(usernameFromJwt);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
user, null,
user.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (RuntimeException e) {
log.error(e.getMessage());
} finally {
filterChain.doFilter(request, response);
}
}
}
Configuration
- Static 파일 경로에 대한 요청 허용
- 회원가입, 로그인페이지로 POST요청 허용
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.secret-key}") private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Static(html, css, js, favicon...) 허용
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
.requestMatchers(
HttpMethod.POST,
"/api/*/user/register", "/api/*/user/login")
.permitAll()
.requestMatchers("/api/**")
.authenticated()
.anyRequest().permitAll()
)
// JWT 필터
.addFilterBefore(new JwtUtil(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.formLogin(withDefaults())
.logout(logout -> logout.logoutSuccessUrl("/"))
// csrf 풀기
.csrf().disable()
.build();
}
}
PasswordEncoder
- 패스워드 인코더를 Bean으로 등록해주어야 함
@Configuration
public class CustomPasswordEncoder {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
왜인지는 모르겠지만... SecurityConfig에 넣으면 Circular Import 때문에 에러가 나서 따로 파일을 분리함
'Java > Spring' 카테고리의 다른 글
[Spring] 간단한 SNS 만들기 #4 (3) | 2023.01.01 |
---|---|
[Spring] 간단한 SNS 만들기 #3 (0) | 2022.12.22 |
[Spring] 간단한 SNS 만들기 #1 (0) | 2022.12.22 |
[Spring] 길찾기 서비스 #6 (0) | 2022.12.11 |
[Spring] 길찾기 서비스 #5 (0) | 2022.12.11 |