-
์ด์ ์คํ๋ง ์ํ๋ฆฌํฐ ์๊ฐ ๊ธ์์ ์ด์ด์ ๊ฐ๋๋ค
[๊ตฌํ] 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์ผ๋ก
'SPRING > PROJECT' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[๊ตฌํ] Swagger 3.0 ์ ์ฉํด๋ณด๊ธฐ (0) 2024.05.06 [๊ตฌํ] Spring Security ์ ์ฐ๋๋ฐ? (0) 2024.05.02 [์ ๋ณด] IntelliJ์์ Querydsl ์ธ ๋ clean build, build ํ ๋ฒ์ ์ํํ๊ธฐ (0) 2023.09.26 [๊ตฌํ] Spring์์ Firebase ํธ์์๋ฆผ ๊ตฌํํ๊ธฐ (0) 2023.09.26 [๊ตฌํ] Spring์์ ์นด์นด์คํก ๋ก๊ทธ์ธ (0) 2023.09.25