ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [๊ตฌํ˜„] Spring Security 6.1 ์ด์ƒ ๋ฒ„์ „ ์ ์šฉ, ์„ค์ •ํ•˜๊ธฐ
    SPRING/PROJECT 2024. 5. 2. 02:07

    ์ด์ „ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์†Œ๊ฐœ ๊ธ€์—์„œ ์ด์–ด์„œ ๊ฐ‘๋‹ˆ๋‹ค

     

    [๊ตฌํ˜„] Spring Security ์™œ ์“ฐ๋Š”๋ฐ?

    Srping Security๋ž€?์ •์˜ by ๊ณต์‹๋ฌธ์„œ๊ฐ•๋ ฅํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ •์˜๊ฐ€ ๊ฐ€๋Šฅํ•œ ์ธ์ฆ ๋ฐ ์•ก์„ธ์Šค ์ œ์–ด ํ”„๋ ˆ์ž„์›Œํฌ Spring ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ์˜ (์‚ฌ์‹ค์ƒ์˜) ํ‘œ์ค€ ํŠน์ง•์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ด๊ณ  ํ™•

    dowlsovo.tistory.com

    SecurityFilterChain ์„ค์ •ํ•˜๊ธฐ

    ์˜์กด์„ฑ ์ถ”๊ฐ€

    **gradle**
    implementation 'org.springframework.boot:spring-boot-starter-security'
    + ํ…Œ์ŠคํŠธ์—์„œ๋„ ํ•„์š”ํ•˜๋‹ค๋ฉด testImplementation 'org.springframework.security:spring-security-test'
    
    **maven**
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

     

    โ€ผ๏ธ ์•„๋ž˜๋ถ€ํ„ฐ๋Š” ์ง€๊ทนํžˆ ์ €์˜ ๊ฐœ์ธ์ ์ธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค

    ํ™•์žฅ์„ฑ์ด ๋†’์€๊ฒŒ ์žฅ์ ์ธ ๊ธฐ์ˆ ์ธ ๋งŒํผ, ํ•„์š”์— ๋”ฐ๋ผ ์ ์ ˆํžˆ ์ˆ˜์ •ํ•ด์„œ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”

    SecurityConfig.java

    : SecurityFilterChain์„ ์„ค์ •ํ•˜๊ณ , ๋ถ™์—ฌ์ฃผ๋Š” ์—ญํ• 

    ์„ค์ • ์ˆœ์„œ์™€๋Š” ๊ด€๊ณ„ ์—†์ด ์ถ”๊ฐ€๋œ ํ•„ํ„ฐ๋“ค์ด ์ •ํ•ด์ง„ ์ˆœ์„œ๋Œ€๋กœ ์ˆ˜ํ–‰๋จ ๋‚ด๊ฐ€ ๋งŒ๋“  ํ•„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋งŒ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋จ

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
            	//cors์˜ ๊ธฐ๋ณธ ๊ฐ’์€ ๋ชจ๋“  ๋ชจ๋“  origin, ํ—ค๋”, HTTP ๋ฉ”์„œ๋“œ(GET, POST, PUT, DELETE ๋“ฑ), ์ž๊ฒฉ ์ฆ๋ช…์„ ํ—ˆ์šฉํ•˜๋ฉฐ, Preflight ์š”์ฒญ์˜ ์ตœ๋Œ€ ์ˆ˜๋ช…์„ ์„ค์ •ํ•˜์ง€ ์•Š์Œ
            	//๋” ์ •๊ตํ•œ ์ •์ฑ…์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ง์ ‘ customํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค
                    .cors(Customizer.withDefaults())
                    //csrf๋Š” ์ฃผ๋กœ SSR์ธ ๊ฒฝ์šฐ์— ํ•„์š”(html ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ)
                    //์„ค์ •ํ•˜๋ฉด ์ข‹๊ฒ ์ง€๋งŒ, disable ํ•ด๋†“๊ฒ ์Šต๋‹ˆ๋‹ค
                    .csrf(AbstractHttpConfigurer::disable)
                    //์„ธ์…˜ ์—†์ด statelessํ•˜๊ฒŒ ์„ค์ •
                    .sessionManagement((sessionManagement) ->
                        sessionManagement
                            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    )
                    //์ธ์ฆ์ด ํ•„์š”ํ•œ url ์„ค์ •
                    .authorizeHttpRequests((authorizeRequests) ->
                            authorizeRequests
    		                        //์•„๋ž˜ url์€ ์ธ์ฆ ํ•„์š”ํ•˜์ง€ ์•Š์Œ
                                .requestMatchers("/", "/auth/**", "/manage/**", "/webjars/**", "/h2-console").permitAll()
                                //๋‚˜๋จธ์ง€๋Š” ์ธ์ฆ ํ•„์š”ํ•จ
                                .anyRequest().authenticated()
                    )
                    //UsernamePasswordAuthenticationFilter(๊ธฐ๋ณธ ํ•„ํ„ฐ) ์ „์— ๊ธฐ๋ณธ ํ•„ํ„ฐ ์ ์šฉ
                    .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    
            return http.build();
        }
        
       
        @Bean
        public WebSecurityCustomizer webSecurityCustomizer() {
            return web -> web.ignoring().requestMatchers(
                    "/swagger-ui/**",
                    "/swagger-resources/**",
                    "/v3/api-docs/**"
            );
        }
    }

    WebSecurityCustomizer ๋นˆ์€ Spring Security์˜ ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์ „์— ์‹คํ–‰๋จ

    • ๋ณด์•ˆ ๊ตฌ์„ฑ์˜ ์ดˆ๊ธฐ ๋‹จ๊ณ„์—์„œ ์ ์šฉ๋˜๋ฉฐ, ๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ ์„ค์ •์— ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ
    • ๋”ฐ๋ผ์„œ, ์—ฌ๊ธฐ์„œ ignore๊ฐ€ ๊ฑธ๋ฆฌ๋ฉด ์ดํ›„ security filter๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Œ → swagger ๊ฐ€ ๋ชจ๋“  api์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Œ

     

    JwtAuthenticationFilter.java

    ์œ„์—์„œ ๋“ฑ๋กํ•œ ํ•„ํ„ฐ

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends GenericFilterBean {
    
        private final JwtTokenProvider jwtTokenProvider;
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
            if (token != null && jwtTokenProvider.validateToken(token)) {
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                context.setAuthentication(authentication);
                SecurityContextHolder.setContext(context);
            }
            chain.doFilter(request, response);
        }
    }

    SecurityContextHolder ๊ทธ๊ฒŒ ๋ญ”๋ฐ?

    ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด

    SecurityContextHolder(static)๋Š” ์ด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š”๋‹ค

    ์–˜๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ๋Š”์ง€, ์ •๋ณด๊ฐ€ ๋ญ”์ง€ ๋‚˜๋ฅผ ์ˆ˜ ์žˆ์Œ

    ๋” ์•Œ๊ณ ์‹ถ๋‹ค๋ฉด ์•„๋ž˜

    Servlet Authentication Architecture :: Spring Security

     

    JwtTokenProvider.java

    jwt ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ, ์ƒ์„ฑ, ๊ฒ€์ฆ, ์ •๋ณด ์ถ”์ถœ ๋“ฑ์˜ ๋ฉ”์†Œ๋“œ๋“ค์„ ํ•œ ๊ณณ์— ๋ชจ์€ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

    autoincreasement id๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋˜ ๋‹ค๋ฅธ ์‹๋ณ„์ž email์„ ๋Œ€์‹  ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

    claim์—๋Š” ์ฃผ๋กœ ROLE์„ ๋งŽ์ด ๋‹ด์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ROLE์ด ํฐ ์—ญํ• ์„ ํ•˜๊ณ  ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜์ค‘์— ๋ฆฌํŒฉํ„ฐ๋ง ํ•˜๋ฉด์„œ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

    @RequiredArgsConstructor
    @Component
    public class JwtTokenProvider {
    		
    		//๋ณด์•ˆ์„ ์œ„ํ•ด ymlํŒŒ์ผ์— ๋‹ด์•˜์Šต๋‹ˆ๋‹ค
        @Value("${security.secret-key.jwt}")
        private String secretKey;
        
        private final long tokenValidTime = 3 * 60 * 60 * 1000L;
    
        private final UserDetailsService userDetailsService;
    
        @PostConstruct
        protected void init(){
            secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
        }	
    
    		//์ƒ์„ฑ
        public String createAccessToken(Member member) {
    		    //jwt์— ๋‹ด์„ ๋‚ด์šฉ ์„ค์ •, ๊ธฐํ˜ธ์— ๋งž์ถฐ ํ•„์š”ํ•œ ๋‚ด์šฉ์„ ๋‹ด์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค
            Claims claims = Jwts.claims().setSubject(member.getEmail()); //์ด๋ฉ”์ผ ๋‹ด์•˜์Šต๋‹ˆ๋‹ค
            Date now = new Date();
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(now)
                    .setExpiration(new Date(now.getTime() + tokenValidTime))
                    .signWith(SignatureAlgorithm.HS256, secretKey)
                    .compact();
        }
    		
    		//์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        public Authentication getAuthentication(String token) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserId(token)); //์•„์ด๋””๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฉ”์ผ ๋ฑ‰์Šต๋‹ˆ๋‹ค
            return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        }
       
        public String getUserId(String token) {
            if(validateToken(token))
                return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); //์ด๋ฉ”์ผ ๋ฑ‰์Šต๋‹ˆ๋‹ค
            //์ €ํฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” custom exception์ž…๋‹ˆ๋‹ค
            throw new CustomException(ResponseCode.UNAUTHORIZED_INVALID_TOKEN);
        }
        
        //ํ—ค๋”์—์„œ ํ† ํฐ ๊บผ๋‚ด์˜ค๊ธฐ
        public String resolveToken(HttpServletRequest request) {
            return request.getHeader("Authorization");
        }
        
        //ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
        public boolean validateToken(String jwtToken) {
            try {
                Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
                return !claims.getBody().getExpiration().before(new Date());
            } catch (Exception e) {
                throw new CustomException(ResponseCode.UNAUTHORIZED_INVALID_TOKEN);
            }
        }
    }

     

    UserDetailService.java ์ปค์Šคํ…€ํ•œ CustomUserDetailService.java

    loadUserByUsername์˜ ์ธ์ˆ˜๋Š” String์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ํ•˜์ง€๋งŒ ์ €ํฌ๋Š” autoincreasement id๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์‹œ ๋งํ•˜์ง€๋งŒ ๋˜ ๋‹ค๋ฅธ ์‹๋ณ„์ž email์„ ๋Œ€์‹  ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

    @RequiredArgsConstructor
    @Service
    public class CustomUserDetailService implements UserDetailsService {
    
        private final MemberMapper memberMapper;
    
        @Override
        public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
            Member member =  memberMapper.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("ํ•ด๋‹น email์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."));
            return new PrincipalDetails(member);
        }
    }
    
    

     

    PrincipalDetails.java

    ํ•˜๋‹จ์˜ isAccountNonExpired, isAccountNonLocked… ๋“ค๋„ member.getExpired()์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฆฌํ„ดํ•˜๋ฉด ์ข‹์•˜๊ฒ ์ง€๋งŒ, ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ๊ธฐ์— ํ•ด๋‹น ๋ถ€๋ถ„์€ ์ƒ๋žตํ•˜๊ณ  ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

    @Getter
    @AllArgsConstructor
    public class PrincipalDetails implements UserDetails {
    
        private Member member;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
    
            Collection<GrantedAuthority> collection = new ArrayList<>();
            collection.add((GrantedAuthority) () -> member.getRole());
            return collection;
        }
    
        @Override
        public String getPassword() {
            return member.getPassword();
        }
    
        @Override
        public String getUsername() {
            return member.getName();
        }
        @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์˜ ์ผ๋ถ€

    @Transactional
    public void join(MemberJoinDto memberJoinDto, MultipartFile image){
        if(memberMapper.existByEmail(memberJoinDto.getEmail()))
            throw new CustomException(BAD_REQUEST_MEMBER_DUPLICATED);
        String imageUrl = null;
        if(image != null) imageUrl = ImagePathUtil.restore(image);
        memberMapper.insert(
                memberJoinDto.getEmail(),
                memberJoinDto.getName(),
                passwordEncoder.encode(memberJoinDto.getPassword()),
                imageUrl
        );
    }
    
    public MemberTokenDto login(MemberLoginDto memberLoginDto) {
      Member member = memberMapper.findByEmail(memberLoginDto.getEmail()).orElseThrow(
      			() -> new CustomException(NOT_FOUND_MEMBER)
      		);
      if(passwordEncoder.matches(memberLoginDto.getPassword(), member.getPassword())){
          return MemberTokenDto.builder()
                  .member(member)
                  .accessToken(jwtTokenProvider.createAccessToken(member))
                  .build();
      }
      throw new CustomException(NOT_FOUND_MEMBER);
      
    }
    

    controller์˜ ์ผ๋ถ€

        @GetMapping("/profile")
        public ResponseEntity<?> getProfile(@AuthenticationPrincipal PrincipalDetails memberPrincipal){
            return ResponseEntity.status(HttpStatus.OK).body(memberService.getProfile(memberPrincipal.getMember()));
    
        }
    

    request.getUserPrincipal()์„ @AuthenticationPrincipal์œผ๋กœ

Designed by Tistory.