TIL

TIL - 2024/06/20

기석김 2024. 6. 20. 17:59

인증/인가도 구현을 했다. 로그인/회원가입과 같이 ,

 

JwtConfig

jwt secret key는 환경변수로 설정해놨다.

@Getter
@Component
public class JwtConfig {

    @Value("${jwt.secret.key}")
    private String secretKey;

    private Key key;

    @PostConstruct
    public void init() {

        byte[] bytes = Base64.getDecoder().decode(secretKey);

        key = Keys.hmacShaKeyFor(bytes);

    }
}

 

WebSecurityConfig이다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final UserRepository userRepository;
    private final BlockedUserRepository blockedUserRepository;

    public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration, UserRepository userRepository,
        BlockedUserRepository blockedUserRepository) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
        this.authenticationConfiguration = authenticationConfiguration;
        this.userRepository = userRepository;
        // this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
        this.blockedUserRepository = blockedUserRepository;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil, userRepository, blockedUserRepository);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));

        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf( (csrf) -> csrf.disable());

        http.sessionManagement( (sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.authorizeHttpRequests( (authorizeHttpRequests) ->
            authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/users/login", "/users/signup", "/api/users/refresh", "/users/login/kakao/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/orders", "/stores").permitAll()
                .requestMatchers("/admin/**").hasAuthority("ADMIN")
                .anyRequest().authenticated()
        );

//        http.exceptionHandling( (exceptionHandling) -> {
//            exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint);
//        });

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterAfter(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);

        return http.build();
    }
}

TokenResponseDto 

/**
 * 토큰 응답 DTO
 * 액세스 토큰과 리프레시 토큰을 포함하는 데이터 전송 객체
 */
@Getter
public class TokenResponseDto {
    private String accessToken;
    private String refreshToken;

    public TokenResponseDto(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

JwtAuthenticationFilter이다.

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;
    private final BlockedUserRepository blockedUserRepository;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository,
        BlockedUserRepository blockedUserRepository) {
        this.jwtUtil = jwtUtil;
        this.userRepository = userRepository;
        this.blockedUserRepository = blockedUserRepository;
        setFilterProcessesUrl("/users/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response) throws AuthenticationException {

        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("잘못된 http 요청입니다.");
        }

        try {

            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(),
                LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(requestDto.getUserId(),
                    requestDto.getPassword(), null));
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }

    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {

        String userId = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        String userName = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUserName();
        UserAuth userAuth = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUserAuth();

        Optional<User> user = userRepository.findByUserId(userId);

        if (user.isEmpty() || user.get().getUserStatus().equals(UserStatus.WITHDRAWN)) {

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("아이디, 비밀번호를 확인해주세요.");

            return;
        }


        Optional<BlockedUser> blockedUser = blockedUserRepository.findByUserId(user.get().getId());
        if (blockedUser.isPresent()) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("해당 사용자는 차단되었습니다.");
            return;
        }

        String accessToken = jwtUtil.generateAccessToken(userId, userName, userAuth);
        String refreshToken = jwtUtil.generateRefreshToken(userId, userName, userAuth);
        ResponseCookie refreshTokenCookie = jwtUtil.generateRefreshTokenCookie(refreshToken);

        user.get().updateRefreshToken(refreshToken);
        userRepository.save(user.get());

        StatusCommonResponse commonResponse = new StatusCommonResponse(200, "로그인 성공");

        response.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
        response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(new ObjectMapper().writeValueAsString(commonResponse));

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException failed)
        throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("아이디, 비밀번호를 확인해주세요.");
    }

}
@Getter
public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserId();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority(user.getUserAuth().toString()));
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {

        User user = userRepository.findByUserId(userId).orElseThrow( () -> new UsernameNotFoundException("아이디, 비밀번호를 확인해주세요."));

        return new UserDetailsImpl(user);
    }

}

JwtUtil 

/**
 * JwtUtil: JWT 토큰을 생성하고 검증하는 유틸리티 클래스
 */
@Component
public class JwtUtil {

    @Value("${jwt.token.expiration}")
    private long ACCESS_TOKEN_TIME;

    @Value("${jwt.refresh.token.expiration}")
    private long REFRESH_TOKEN_TIME;

    public static final String BEARER_PREFIX = "Bearer ";
    public static final String AUTHORIZATION_KEY = "auth";
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    private final JwtConfig jwtConfig;
    private final Key key;

    public JwtUtil(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
        this.key = jwtConfig.getKey();
    }

    public String generateToken(String userId, String userName, UserAuth userAuth, long tokenTime) {

        Date date = new Date();

        return Jwts.builder()
            .setSubject(userId)
            .claim("userName", userName)
            .claim(AUTHORIZATION_KEY, userAuth.toString())
            .setExpiration(new Date(date.getTime() + tokenTime))
            .setIssuedAt(date)
            .signWith(key, signatureAlgorithm)
            .compact();
    }

    public String generateAccessToken(String userId, String userName, UserAuth userAuth) {
        return generateToken(userId, userName, userAuth, ACCESS_TOKEN_TIME);
    }

    public String generateRefreshToken(String userId, String userName, UserAuth userAuth) {
        return generateToken(userId, userName, userAuth, REFRESH_TOKEN_TIME);
    }

    public String generateNewRefreshToken(String userId, String userName, UserAuth userAuth, Date expirationDate) {

        Date date = new Date();

        return Jwts.builder()
            .setSubject(userId)
            .claim("userName", userName)
            .claim(AUTHORIZATION_KEY, userAuth.toString())
            .setExpiration(expirationDate)
            .setIssuedAt(date)
            .signWith(key, signatureAlgorithm)
            .compact();
    }

    public ResponseCookie generateRefreshTokenCookie(String refreshToken) {
        return ResponseCookie.from("refreshToken", refreshToken)
            .httpOnly(true)
            .maxAge(REFRESH_TOKEN_TIME / 1000)
            .path("/")
            .sameSite("Strict")
            .build();
    }

    public ResponseCookie generateNewRefreshTokenCookie(String refreshToken, Date expirationDate) {

        long maxAge = (expirationDate.getTime() - new Date().getTime()) / 1000;

        return ResponseCookie.from("refreshToken", refreshToken)
            .httpOnly(true)
            .maxAge(maxAge)
            .path("/")
            .sameSite("Strict")
            .build();
    }

    public String getAccessTokenFromHeader(HttpServletRequest request) {

        String authorizationHeader = request.getHeader("Authorization");

        if (StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX)) {
            return authorizationHeader.substring(7);
        }

        return null;
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public void checkTokenExpiration(String token) throws TokenExpiredException {

        try {

            Claims claims = getClaimsFromToken(token);
            Date date = claims.getExpiration();
            Date now = new Date();

            if (date != null && date.before(now)) {
                throw new TokenExpiredException("토큰이 만료되었습니다.");
            }

        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("토큰이 만료되었습니다.");
        }

    }

    public Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}