현재 회사에서 사용하는 로그인 방식을 바꾸는데 내가 구현 할 확률이 높기에 okta 공식 사이트를 알아보는 중 공식 가이드에서도 Spring Security예제 코드를 사용하고 권장하는바에 해야지 생각만하다가 강의와 각종 정리한 걸 챙겨보며 정리를 해 보았다.
security config 설정
기존에는 WebSecurityConfigureAdapter을 상속받아서 구현하는 방식을 사용하지만 버전이 바뀌며
deprecated 되면서 SecurityFilterChain을 bean 으로직접 등록하는 방식을 지향하기에 bean 방식을 사용해보았다.
package com.example.demo.Form.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig2 {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors().disable()//cors 설정
.csrf().disable()// csrf 설정
.sessionManagement() // 세션 설정 시작 메소드
.maximumSessions(1) //최대 허용가능 세션 메소드 -1 입력시 무제한 으로 생성이 가능
// 설정한 허용 세션의 수가 넘어갈 시 처리할 메소드 true 면 현재 사용자 인증실패
// false(default) 면 기존 세션이 만료하게 된다
.maxSessionsPreventsLogin(true)
.expiredUrl("/login") //세션이 만료하면 이동할 주소
.and()
.and() //and 를 두개를 사용해야 다음 설정이 가능해지기에 2개를 사용했다
.authorizeRequests()//권한에 맞는 접속 설정
.antMatchers("/member/**").hasRole("USER") //이 경로상에는 이 권한에 맞는 유저 접속 가능
.anyRequest().authenticated() // 다른 모든 요청들은 인증된 사용자만 접근 할 수 있다.
.and()
.formLogin()
.loginPage("/loginForm") // 사용자 정의 로그인 페이지 GET
.defaultSuccessUrl("/") // 로그인 성공 후 이동 페이지
.failureUrl("/login") // 로그인 실패 후 이동 페이지
.usernameParameter("userId") // 아이디 파라미터명 설정
.passwordParameter("passwd") // 패스워드 파라미터명 설정
.loginProcessingUrl("/loginForm") // 로그인 Form Action Url POST
// 로그인 성공 후 핸들러
.successHandler((request, response, authentication) -> response.sendRedirect("/"))
// 로그인 실패 후 핸들러
.failureHandler((request, response, exception) -> response.sendRedirect("/login"))
.permitAll() // 접근 모두 허용
.and()
.logout() // 로그아웃 처리
.logoutUrl("/logout") // 로그아웃 처리 URL
.logoutSuccessUrl("/login") // 로그아웃 성공 후 이동 페이지
// 로그아웃 핸들러 세션 삭제 용도 테스트
.addLogoutHandler((request, response, authentication) -> request.getSession().invalidate())
// 로그아웃 성공 후 핸들러 리다이렉트 용도 테스트
.logoutSuccessHandler((request, response, authentication) -> response.sendRedirect("/login"))
.invalidateHttpSession(true)// true 설정 시 로그아웃 후 session 을 날린다
.deleteCookies("remember-me") // 로그아웃 후 지정한 이름의 쿠키 삭제
.and()
.build();
}
// 패스워드 암호화하는 빈 등록
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
접근 지정 메소드
어떠한 접근을 할 때 접근 권한을 설정 할 수 있게 Spring Security 에서 아래에 메소드들을 지원해준다
- anonymous()
인증되지 않은 사용자가 접근할 수 있습니다. - authenticated()
인증된 사용자만 접근할 수 있습니다. - fullyAuthenticated()
완전히 인증된 사용자만 접근할 수 있습니다(?) - hasRole() or hasAnyRole()
특정 권한을 가지는 사용자만 접근할 수 있습니다. - hasAuthority() or hasAnyAuthority()
특정 권한을 가지는 사용자만 접근할 수 있습니다. - hasIpAddress()
특정 아이피 주소를 가지는 사용자만 접근할 수 있습니다. - access()
SpEL 표현식에 의한 결과에 따라 접근할 수 있습니다. - not() 접근 제한 기능을 해제합니다.
- permitAll() or denyAll()
접근을 전부 허용하거나 제한합니다. - rememberMe()
리멤버 기능을 통해 로그인한 사용자만 접근할 수 있습니다.
Role은 역할이고 Authority는 권한이지만 사실은 표현의 차이입니다. Role은 “ADMIN”으로 표현하고 Authority는 “ROLE_ADMIN”으로 표기하는 것 뿐입니다. 실제로 hasRole()에 ROLE_ADMIN으로 표기하면 ROLE을 지우라는 에러를 볼수 있게 됩니다.
Config 에서 Spring Sccurity 에 대한 핵심설정을 다할 수 있기에 가장 중요한 부분이라고 생각을 한다.
코드는 사용을 많이 할 메소드를 선정을 해서 주석으로 설명을 해 놓았다.
SpringBoot3.1, Spring Security6.1.0 SecurityFilterChain 변화
최신 스프링 시큐리티 에서는 이런 체이닝 메소드문법이 아니라 람다DSL이라는 문법을 사용하는 것으로 변경을 했다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/member/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage("/loginForm")
.permitAll()
)
.rememberMe(Customizer.withDefaults());
return http.build();
}
}
장점
- 각 메소드마다의 역할을 좀 더 명확하게 코드상에서 드러나는 것 같다
- and()를 사용할 필요가 없다
단점
- 개인적인 의견으로서는 내가 설정한 파일처럼 들여쓰기를 통해 각 메소드마다 역할을 명확하게 보여 줄 수 있다고 생각한다.
- 체이닝 메소드문법이 깔끔다고 생각하는 입장에서는 람다식으로 하는 게 더 명확해보이진 않는다.
변경된 이유
기존 방식에서는 어떤 반환타입이 나올지 모르고 어떤 객체가 구성되고 있는지 명확하지 않았다.
중첩이 깊어질수록 더 혼란스러워졌고 숙련된 사용자도 파악을 하기가 힘들었다고 한다.
아직 스프링 부트 2.x버전을 사용하는 곳이 많기에 나는 이전 버전의 방식을 채택했다.
WebIgnore
js / css / image File 등 Security Filter를 적용할 필요가 없는 파일에 대한 설정.
Spring Security는 Project 내에 모든 Code에 대해 보안 Filter를 하도록 기본적으로 설정이 되어 있다.
그래서 js / css / image 등과 같은 File도 따로 설정을 해 주지 않으면 Spring Security Filter에서 해당 이용자가 접근 여부를 검사하게 되어 있다.
그래서 위와 같이 보안 설정이 필요 없는 설정을 추가해주어야 합니다.
Spring Security 5.7 이하 Version 사용법
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.requestMatchers(PathRequest.toStaticResources()
.atCommonLocations());
}
Spring Security 5.7 이상 Version 사용법
import com.junyharang.springsecurityformauth.constant.ServiceURIManagement;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity // Spring Security 설정 활성화
@Configuration
public class WebSecurityConfigure {
@Bean
public WebSecurityCustomizer websecurityCustomizer() {
return (web) -> web.ignoring()
.antMatchers("/resources/**");
}
}
UserDetailsServiceImpl
package com.practice.board.service.Impl;
import com.practice.board.domain.Member;
import com.practice.board.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
Member member = memberRepository.findByMemberId(memberId)
.orElseThrow(() -> new UsernameNotFoundException("사용자가 존재하지 않습니다."));
return toUserDetails(member);
}
private UserDetails toUserDetails(Member member) {
return User.builder()
.username(member.getMemberId())
.password(member.getPassword())
.authorities(new SimpleGrantedAuthority(member.getRole().toString()))
.build();
}
}
UserDetailService 를 상속받아서 loadUserByname메소드 구현 코드
비밀번호 검증로직이 없는데 틀리면 로그인이 안돼기에 궁금해 찾아본결과
loadUserByname 비밀번호 검증
request가 오고 AuthenticationManager.authenticate(Authentication)을 호출하면
스프링 시큐리티에 내장된 AuthenticationProvider의 authenticate()메서드가 호출되는데,
DaoAuthenticationProvider.additionalAuthenticationChecks 메서드 에서 검증 코드가 있다.
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
역시 최근 프레임워크들은 추상화가 너무 심해 많은 것들을 지원해줘서 편하긴 하지만 이유를 모르고 쓰기엔 안될 것 같다.
당연히 AuthenticationProvider를 상속받아서 직접 커스텀클래스를 만들고 비밀번호 검증을 직접하고 원하는 Exception도 설정을 할 수가 있다.
이후에는 SecurityContext에 저장되어 값을 가져 올 수 있다.
'Spring > spring security' 카테고리의 다른 글
Spring Security JWT 로그인 (0) | 2024.01.25 |
---|