[๊ตฌํ] Spring Security 6.1 ์ด์ ๋ฒ์ ์ ์ฉ, ์ค์ ํ๊ธฐ
์ด์ ์คํ๋ง ์ํ๋ฆฌํฐ ์๊ฐ ๊ธ์์ ์ด์ด์ ๊ฐ๋๋ค
[๊ตฌํ] 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์ผ๋ก