SPRING/PROJECT

[๊ตฌํ˜„] Spring Security 6.1 ์ด์ƒ ๋ฒ„์ „ ์ ์šฉ, ์„ค์ •ํ•˜๊ธฐ

ozllzL 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์œผ๋กœ