코드 그라데이션

[중간점검] JWT 2.41 버전 구현 로직 완성 코드 본문

Spring/Security

[중간점검] JWT 2.41 버전 구현 로직 완성 코드

완벽한 장면 2023. 12. 15. 00:57

Entity

User

@Entity
@Table(name = "user")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @JsonIgnore
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(name = "username", length = 50, unique = true)
    private String username;

    @JsonIgnore
    @Column(name = "password")
    private String password;

    @Column(name = "nickname", length = 50)
    private String nickname;

    @JsonIgnore
    @Column(name = "activated")
    private boolean activated;

    @ManyToMany
    @JoinTable( // 다대다를 일대다, 다대일 관계로 재정의했다는 뜻.
            name = "user_authority",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;

}

 

Authority

@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {

    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName; // 권한 이름
}

 


Repository

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {

    // 해당 쿼리 수행 될 때 Eager 조회로 authorities 정보를 같이 가져오게 된다.
    @EntityGraph(attributePaths = "authorities")
    Optional<User> findOneWithAuthoritiesByUsername(String username);
    //username을 기준으로 User 정보를 가져올 때 권한 정보도 같이 가져오게 된다.
}

 


Service

UserService

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;


    @Transactional
    public User signup(UserDto userDto) {
        if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
            throw new RuntimeException("이미 가입되어 있는 유저입니다.");
        }
        // 권한 정보 생성
        Authority authority = Authority.builder()
                .authorityName("ROLE_USER")
                .build();

        // 유저 정보 생성
        User user = User.builder()
                .username(userDto.getUsername())
                .password(passwordEncoder.encode(userDto.getPassword()))
                .nickname(userDto.getNickname())
                .authorities(Collections.singleton(authority))
                .activated(true)
                .build();

        // 저장
        return userRepository.save(user);
    }

    /**
     *
     * @param username
     * @return
     * username을 기준으로 정보를 가져오는 메서드
     */
    @Transactional(readOnly = true)
    public Optional<User> getUserWithAuthorities(String username) {
        return userRepository.findOneWithAuthoritiesByUsername(username);
    }

    /**
     * SecurityContext에 저장된 username의 정보만 가져온다.
     * @return
     */
    @Transactional(readOnly = true)
    public Optional<User> getMyUserWithAuthorities() {
        return SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername);
    }

}

 

 

CustomUserDetailsService

@Component("userDetailsService")
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // UserRepository를 사용하여 사용자 정보를 데이터베이스에서 검색
        return userRepository.findOneWithAuthoritiesByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    // 사용자 정보를 기반으로 UserDetails 객체를 생성하는 메서드
    private org.springframework.security.core.userdetails.User createUser(String username, User user) {
        if (!user.isActivated()) { // 사용자가 활성화되지 않은 경우 예외 던짐
            throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
        }

        // 사용자의 권한 정보를 가져와서 SimpleGrantedAuthority 객체로 변환
        List<GrantedAuthority> grantedAuthorities = user.getAuthorities()
                .stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toList());

        // UserDetails 인터페이스를 구현한 객체를 생성하여 반환
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(), grantedAuthorities);
    }
}

 

 


DTO

LoginDto

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {

    @NotNull
    @Size(min = 3, max = 50) // 유효성 검사
    private String username;

    @NotNull
    @Size(min = 3, max = 100)
    private String password;

}

 

 

TokenDto

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenDto {

    private String token;
}

 

 

UserDto

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {

    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @NotNull
    @Size(min = 3, max = 100)
    private String password;

    @NotNull
    @Size(min = 3, max = 50)
    private String nickname;
}

 


Controller

AuthController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AuthController {

    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;


    @PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {

        // 사용자가 제공한 로그인 정보(username과 password)를 사용하여
        // UsernamePasswordAuthenticationToken을 생성
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        // AuthenticationManagerBuilder를 사용하여
        // 생성된 authenticationToken을 인증
        Authentication authentication
                = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 인증에 성공한 경우, SecurityContextHolder에 현재 인증 정보를 설정
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // TokenProvider를 사용하여 JWT(JSON Web Token)를 생성
        String jwt = tokenProvider.createToken(authentication);

        // HTTP 응답 헤더에 JWT 토큰을 추가
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

        // JWT 토큰을 포함한 TokenDto를 응답으로 반환
        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
    }
}

 

 

UserController

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {

    private final UserService userService;

    /**
     * 사용자 회원가입을 처리하는 엔드포인트.
     *
     * @param userdto 회원가입 요청에 필요한 사용자 정보를 담은 DTO 객체
     * @return 회원가입이 성공하면 사용자 정보를 담은 ResponseEntity를 반환
     */
    @PostMapping("/signup")
    public ResponseEntity<User> siginup(@Valid @RequestBody UserDto userdto) {
        return ResponseEntity.ok(userService.signup(userdto));
    }



    /**
     * 현재 사용자의 정보를 조회하는 엔드포인트.
     * 사용자 및 관리자 역할을 가진 사용자만 접근할 수 있다.
     *
     * @return 현재 사용자의 정보를 담은 ResponseEntity를 반환
     */
    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public ResponseEntity<User> getMyUserInfo() {
        // userService를 사용하여 현재 사용자의 정보를 조회하고, 그 결과를 ResponseEntity로 반환.
        // getMyUserWithAuthorities()는 현재 사용자의 정보와 권한 정보를 함께 조회하는 서비스 메서드.
        // 반환된 정보는 ResponseEntity.ok()를 사용하여 HTTP 200 OK 상태와 함께 반환됨.
        return ResponseEntity.ok(userService.getMyUserWithAuthorities().get());
    }



    /**
     * 특정 사용자의 정보를 조회하는 엔드포인트.
     * 관리자 역할을 가진 사용자만 접근할 수 있다.
     *
     * @param username 조회할 사용자의 이름
     * @return 특정 사용자의 정보를 담은 ResponseEntity를 반환
     */
    @GetMapping("/user/{username}")
    @PreAuthorize("hasAnyRole('ADMIN')")
    public ResponseEntity<User> getUserInfo(@PathVariable String username) {
        // userService를 사용하여 특정 사용자의 정보를 조회하고, 그 결과를 ResponseEntity로 반환함.
        // getUserWithAuthorities(username)는 특정 사용자 정보와 권한 정보를 함께 조회하는 서비스 메서드.
        // 반환된 정보는 ResponseEntity.ok()를 사용하여 HTTP 200 OK 상태와 함께 반환됨.
        return ResponseEntity.ok(userService.getUserWithAuthorities(username).get());
    }
}

 

 

 


Config

SecurityConfig

@EnableWebSecurity // 기본적인 웹 시큐리티 설정을 활성화하겠다.
// 추가적 설정을 위해서 WebSecurityConfigurer 을 implement 하거나
// WebSecurityConfigureAdapter 를 extends 하는 방법이 있다.
@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize 어노테이션을 메서드 단위로 추가하기 위해서 사용
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * h2-console 하위 모든 요청들과 파비콘 관련 요청은
     * Spring Security 로직을 수행하지 않고 접근할 수 있게 configure 메서드를 오버라이드해서 내용을 추가해준다.
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .antMatchers(
                        "/h2-console/**"
                        ,"/favicon.ico"
                        ,"/error"
                );
    }

    /**
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception
     *
     * 많은 부분들을 추가
     * : 토큰을 사용하기 때문에 csrf 설정은 disable
     *  Exception을 핸들링할 때 만들었던 클래스들을 추가한다.
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // token을 사용하는 방식이기 때문에 csrf를 disable한다.
        http.csrf().disable()
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // h2-console을 위한 설정
                .and()
                .headers()
                .frameOptions()
                .sameOrigin()


                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session을 사용하지 않음.

                .and()
                .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근 제한을 설정하겠다.
                .antMatchers("/api/hello").permitAll() // /api/hello 에 대한 요청은 인증 없이 접근을 허용하겠다.
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/api/signup").permitAll()
                .anyRequest().authenticated() // 나머지 요청들에 대해서는 인증을 받아야 한다.

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));
    }
}

 

 

CorsConfig

@Configuration
public class CorsConfig {

    // CORS 필터 빈을 정의하는 메서드
    @Bean
    public CorsFilter corsFilter() {
        // URL 기반의 CORS 구성을 관리하는 객체 생성
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        // CORS 구성 설정 객체 생성
        CorsConfiguration config = new CorsConfiguration();

        // 요청에서 자격 증명(자격 증명 쿠키, 인증 등)을 허용
        config.setAllowCredentials(true);

        // 모든 출처(Origin)를 허용
        config.addAllowedOrigin("*");

        // 모든 HTTP 헤더를 허용
        config.addAllowedHeader("*");

        // 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용
        config.addAllowedMethod("*");

        // "/api/**" 패턴에 해당하는 엔드포인트에 대해 CORS 구성을 등록
        source.registerCorsConfiguration("/api/**", config);

        // CORS 필터를 생성하고 반환
        return new CorsFilter(source);
    }
}

 

 


Util

SecurityUtil

@NoArgsConstructor
public class SecurityUtil {
    private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    /**
     * 이 메서드의 역할은 Security Context의 Authentication 객체를 이용해 username을 리턴해주는 간단한 유틸성 메서드
     *
     * @return 현재 사용자의 username을 Optional<String> 형태로 반환. 인증 정보가 없으면 Optional.empty() 반환.
     */
    public static Optional<String> getCurrentUsername() {
        // 현재의 Authentication 객체를 가져온다.
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 인증 정보가 없는 경우
        if (authentication == null) {
            logger.debug("Security Context에 인증 정보가 없습니다.");
            return Optional.empty(); // 빈 Optional 반환
        }

        String username = null;

        // Authentication 객체의 주체(principal)를 확인
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
            username = springSecurityUser.getUsername(); // UserDetails에서 username을 가져옴
        }
        else if (authentication.getPrincipal() instanceof String) {
            username = (String) authentication.getPrincipal(); // 문자열로 된 주체의 경우, username으로 설정
        }

        return Optional.ofNullable(username); // Optional로 username 반환 (null인 경우에도 처리)
    }
}
728x90
Comments