CS 공부 & 기초 지식

[Spring Boot] Spring Security를 이용하여 인증인가 구현하기

Coding-Su 2024. 11. 15. 17:01
728x90

Spring Security란?

Spring Security는 인증, 인가를 구현하는 데 도움을 주는 Spring의 프레임워크입니다.

Spring Security를 이용하면 전에 구현했던 어노테이션을 만드는 과정도 매우 쉽게 구현할 수 있습니다.

 

이전에 JWT를 이용한 인증인가

 

[Spring Boot] JWT를 이용한 인증/인가 구현하기(Spring Security X)

JWT를 사용하기 위한 파일FilterConfig.java@Configuration@RequiredArgsConstructorpublic class FilterConfig { private final JwtUtil jwtUtil; @Bean public FilterRegistrationBean jwtFilter() { FilterRegistrationBean registrationBean = new FilterRegistr

coding-su.tistory.com

 

 

Spring Security를 이용한 인증/인가 구현하기

위에 있는 전에 사용한 코드를 변경하여 구현하겠습니다.

 

JwtUtil.java

@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();
    }
}

 

SecurityConfig.java

@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();
    }
}

 

Filter를 등록해줍니다. 여기서 여러가지 내용을 비활성화 하는 이유는 Spring Security는 JWT 토큰 방식보다는 세션 방식에 조금 더 치중되어 있어 세션기능에서 필요한 기능은 JWT 토큰 방식에서는 필요 없기 때문에 비활성화 하였습니다.

비활성화 하지 않아도 문제 없습니다.

 

permitAll은 전에 Spring Security 없이 구현했을 때와 마찬가지로 로그인과 회원가입은 Filter의 검증을 통과해야 하기 때문에 permitAll로 통과시켜주어야 합니다.

 

처음 Spring Security를 적용하시는 분들이 permitAll 설정을 하지 않아 로그인과 회원가입도 막히는 경우가 있기 때문에 꼭 설정을 해야합니다.

 

JwtSecurityFilter.java

@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);
                Long userId = Long.valueOf(claims.getSubject());
                String email = claims.get("email", String.class);
                UserRole userRole = UserRole.of(claims.get("userRole", String.class));

                if (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);
    }
}

 

Filter를 만들어 줍니다.

 

JwtAuthenticationToken.java

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;
    }
}

 

Argument Resolver입니다. Spring Security를 사용하지 않을 때는 많은 설정이 필요했지만 Spring Security를 사용하면 위 파일 하나로 해결할 수 있습니다.

 

또한 Spring Security를 사용하지 않았을 때는 PasswordEncoder를 사용했지만 Spring Security에서는 기본적으로 제공하기 때문에 구현할 필요가 없습니다.

 

 

 

Spring Security를 사용하여 인증/인가, 메모를 구현한 코드

https://github.com/SuHyun-git/spring-basecode/tree/study/springsecurity

 

GitHub - SuHyun-git/spring-basecode

Contribute to SuHyun-git/spring-basecode development by creating an account on GitHub.

github.com

 

728x90