환경세팅
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
spring secrity 를 사용하기 위한 의존성 주입
JwtUtil
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createToken(Long userId, String email, UserRole userRole) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole.getUserRole())
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
JwtSecurityFilter
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtSecurityFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(
HttpServletRequest httpRequest,
@NonNull HttpServletResponse httpResponse,
@NonNull FilterChain chain
) throws ServletException, IOException {
String authorizationHeader = httpRequest.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String jwt = jwtUtil.substringToken(authorizationHeader);
try {
Claims claims = jwtUtil.extractClaims(jwt);
String userId = claims.getSubject();
String email = claims.get("email", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
AuthUser authUser = new AuthUser(userId, email, userRole);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (Exception e) {
log.error("Internal server error", e);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
chain.doFilter(httpRequest, httpResponse);
}
}
SecurityConfig
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
private final JwtSecurityFilter jwtSecurityFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // SessionManagementFilter, SecurityContextPersistenceFilter
)
.addFilterBefore(jwtSecurityFilter, SecurityContextHolderAwareRequestFilter.class)
.formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
.anonymous(AbstractHttpConfigurer::disable) // AnonymousAuthenticationFilter 비활성화
.httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화
.logout(AbstractHttpConfigurer::disable) // LogoutFilter 비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/signin", "/auth/signup").permitAll()
.requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
.anyRequest().authenticated()
)
.build();
}
}
JWT 토큰을 이용하는 방식이기 때문에 세션방식처럼 DB 에 접근을 할 필요가 없다.
그래서 disable 로 설정해준 옵션들이 많다.
기존에 Bycript 를 이용해서 사용했던 PasswordEncoder 대신에 security 에서는 이미 해당 클래스를 제공해주기 때문에 위처럼 대체해도 된다.
import 추가
import org.springframework.security.crypto.password.PasswordEncoder;
SecurityConfig
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/signin", "/auth/signup").permitAll()
.requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
.anyRequest().authenticated()
)
하단에 authorizeHttpRequests 옵션을 보면 해당 URI 는 인증되지 않아도 접근을 허가해주는 것이다.
SecurityConfig
.requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
이는 유저 권한이 ADMIN 인 경우에만 허가시켜준다.
permitAll() 로 해놓고
SecurityConfig
@EnableMethodSecurity(securedEnabled = true)
Config 클래스 위에 해당 어노테이션을 달아준다음에 직접 /test Controller 로 가서
TestController
@Slf4j
@RestController
public class TestController {
@Secured(UserRole.Authority.ADMIN)
@GetMapping("/test")
public void test(@AuthenticationPrincipal AuthUser authUser) {
log.info("User ID: {}", authUser.getUserId());
log.info("Email: {}", authUser.getEmail());
log.info("Authorities: {}", authUser.getAuthorities());
}
}
@Secured 어노테이션을 다는 방법도 있다.
장단점이 있으니 둘 중 하나 고르면 된다.
UserRole
@Getter
@RequiredArgsConstructor
public enum UserRole {
ROLE_USER(Authority.USER),
ROLE_ADMIN(Authority.ADMIN);
private final String userRole;
public static UserRole of(String role) {
return Arrays.stream(UserRole.values())
.filter(r -> r.name().equalsIgnoreCase(role))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 UserRole"));
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
Spring Security 에서 정해놓은 UserRole 틀이 있다. 그렇기 때문에 앞에 ROLE_ 을 붙여서 ENUM 값을 맞춰준다.
AuthUser
@Getter
public class AuthUser {
private final String userId;
private final String email;
private final Collection<? extends GrantedAuthority> authorities;
public AuthUser(String userId, String email, UserRole role) {
this.userId = userId;
this.email = email;
this.authorities = List.of(new SimpleGrantedAuthority(role.name()));
}
}
JwtAuthenticationToken
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final AuthUser authUser;
public JwtAuthenticationToken(AuthUser authUser) {
super(authUser.getAuthorities());
this.authUser = authUser;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return authUser;
}
}
Spring Security 로 부터 authUser 값을 가져오기 위해서 위 두 파일을 만들어준다.
'컴퓨터 프로그래밍 > Spring' 카테고리의 다른 글
[Spring] TransactionalEventListener (0) | 2024.10.11 |
---|---|
[Spring] Projection 및 예시코드 설명 (0) | 2024.10.07 |
[Spring] QueryDSL (1) | 2024.10.03 |
[Spring] Lazy Loading 과 Eager Loading 의 차이, N+1 Problem 과 해결 방법 (1) | 2024.10.03 |
[Spring] Password Encoder 사용법 (0) | 2024.09.29 |