임도현의 성장
Spring-Boot Spring Security 스프링 시큐리티 본문
Spring Security 란?
Spring Security는 인증, 인가 그리고 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 사용자 관리 기능을 구현하는데 도움을 주는 스프링 하위 프레임워크입니다. 스프링 시큐리티는 필터 기반으로 동작한다.
🤗 인증과 인가
- 인증(Authentication)은 사용자의 신원을 입증하는 과정입니다. 예를 들어 사용자가 사이트에 로그인을 할 때 누구인지 확인하는 과정을 인증이라고 합니다.
- 인가(Authorization)는 인증과는 다릅니다. 인가는 사이트의 특정 부분에 접근할 수 있는지 권한을 확인하는 작업입니다.
⭐ build.gradle 추가
https://mvnrepository.com/ <== 빌드 종합 세트
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
🚀 실행
- 처음에 바로 실행을 하게되면 밑에 이미지 처럼 로그인페이지가 나온다.
- 이유는 특정한 경로에 요청이 오면 Controller 클래스에 도달하기 전 필터에서 Spring Security가 검증을 함
- 스프링 시큐리티는 필터 기반이므로 요청이오면 바로 반응을 한다.
- id는 user이고 password는 터미널에 나왔있다.
- Spring Security에서 웹사이트 보안에 가장 기본적인 기능인 아이디/패스워드 인증을 화면까지 지원합니다.
🌇 SecurityConfig 인가 작업
- @Configuration은 이 파일이 스프링의 환경 설정 파일임을 의미하는 애너테이션이다.
- @EnableWebSecurity는 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션이다.
- SecurityFilterChain 클래스가 동작하여 모든 요청 URL에 이 클래스가 필터로 적용되어 URL별로 특별한 설정을 할 수 있게 된다.
- 밑에 설명을 보면 permitAll()을 사용하여 "/" 경로 는 접근을 허용시켜 이제 로그인 페이지가 안뜨게 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login", "/join", "/joinProc", "/loginProc").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
return http.build();
}
}
- authorizeHttpRequests : 각 URL 요청에 대한 접근 권한을 설정합니다.
- requestMatchers : 통해 특정 URL에 대해 접근을 허용할지, 특정 역할이 필요할지 정의합니다.
- permitAll() : 해당 URL은 모두 접근할 수 있게 허용합니다.
- hasRole("ADMIN") : /admin 경로에 접근하려면 ADMIN 역할이 필요하다는 의미입니다.
- authenticated() : 나머지 모든 요청은 인증이 필요하다는 설정입니다.
🚫연결 거부
permitAll()을 사용하여 접근을 허용했지만 "/", "/login", "join" 등등 말고는 다른 경로로 들어가면 밑에 사진 처럼 연결을 거부해버린다.
requestMatchers("/", "/login", "/join", "/joinProc", "/loginProc").permitAll()
🥳 커스텀 로그인 설정
SecurityConfig 안에 filterChain매서드 안에 추가해주면 된다.
- formLogin : 커스텀 로그인 페이지를 설정할 때 사용합니다.
- loginPage("/login") : 로그인 페이지로 사용할 URL을 지정합니다. 즉 접근 제한 된 주소로 들어가면 로그인 페이지로 리다이렉트 합니다.
- loginProcessingUrl("/loginProc") : 실제 로그인 처리를 할 URL입니다.
- permitAll() : 로그인 페이지와 로그인 처리는 모두 접근할 수 있게 허용합니다
http
.formLogin((auth) -> auth.loginPage("/login")
.loginProcessingUrl("/loginProc")
.permitAll()
);
👹CSRF 설정
CSRF 방어 기능을 비활성화합니다. 개발 및 테스트 중에는 필요하지 않을 수 있지만, 운영 환경에서는 보안상 활성화하는 것이 좋습니다.
http
.csrf((auth) -> auth.disable()
);
🎅 회원가입 구현 및 비밀번호 암호화
SecurityConfig 안에 빈 등록 해주면 된다.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Data
public class JoinDTO { // 로그인한 아이디와 비밀번호를 받아오는 JoinDTO
private String username;
private String password;
}
@Entity(name = "USERENTITY")
@Data
public class UserEntity { // 데이터 베이스에 넣어줄 Entity
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
private String password;
private String role; // 권한 지정 값
}
👻Service 구현
bCryptPasswordEncoder.encode(joinDTO.getPassword()) 평문을 암호문으로 바꿔주는 코드
@Slf4j
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO){
//db에 이미 동일한 username을 가진 회원이 존재하는지?
boolean isUser = userRepository.existsByUsername(joinDTO.getUsername());
if (isUser) {
return;
}
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
// 암호화
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_ADMIN");
log.info("data = {}", data);
userRepository.save(data);
}
👨💻Repository JPA로 구현
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
// 아이디 증복 검즘
boolean existsByUsername(String username);
// 유저 찾기
UserEntity findByUsername(String username);
}
🐧 JPA 자주 사용하는 쿼리 메서드 명 규칙
Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull, Null | findByAge(Is)Null | … where x.age is null |
IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
🐦 사용자의 정보를 가져오는 UserDetailsService인터페이스 구현
- 스프링 시큐리티에서 로그인을 진행할 때 사용자 정보를 가져와 findByUsername으로 확인을 한다.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByUsername(username);
if (userData != null) { // userData 정보를 보내 줌
return new CustomUserDetails(userData);
}
return null;
}
}
🍟사용자 정보 설정
- 사용자 정보 설정
- CustomUserDetails 생성자를 통해 UserEntity 객체를 받아오고, 이 정보를 기반으로 사용자 인증에 필요한 데이터를 반환합니다.
- 권한 설정 (getAuthorities)
- 이 메서드는 사용자의 권한 정보를 제공합니다. Spring Security는 인증 후 권한에 따라 사용자가 접근할 수 있는 URL을 제한하거나 허용합니다.
- 여기서는 userEntity.getRole()에서 가져온 권한 문자열을 GrantedAuthority로 변환해 권한 리스트로 반환합니다.
- 기본 사용자 정보 제공
- getPassword()와 getUsername() 메서드를 통해 사용자의 비밀번호와 사용자 이름 정보를 Spring Security에 제공합니다.
- 이 정보는 로그인 시 PasswordEncoder를 통해 입력된 평문 비밀번호와 암호화된 DB의 비밀번호를 비교하는 데 사용됩니다.
public class CustomUserDetails implements UserDetails {
//UserEntity 객체를 받아오기
private UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity){
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override // 권한
public String getAuthority() {
// userEntity.getRole()에서 가져온 권한 문자열을 GrantedAuthority로 변환해 리턴
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
🤮 내 순서 정리
- 회원가입 요청
- 클라이언트는 회원가입 폼에 정보를 입력하고, @PostMapping으로 서버로 전달
- Service에서 증복 회원인지 DB에서 찾아봄 없는 회원이면 DB에 저장 이때 비밀번호는 암호문으로 저장 됨
- 회원가입이 완료되면 클라이언트는 로그인 페이지로 리다이렉트됩니다.
- 로그인 요청
- 클라이언트가 username과 password를 입력하고 제출 이 요청은 @PostMapping /loginProc으로 보내지만, 실제 처리는 Spring Security가 자동으로 로그인 요청을 가로채 진행합니다.
- Spring Security는 /loginProc에 대한 POST 요청을 받으면 CustomUserDetailsService에서 DB를 통해 해당 username을 가진 사용자를 조회해서 UserEntity객체를 CustomUserDetails로 보내줍니다.
- 비밀번호 검증
- CustomUserDetails에 서 가져온 UserEntity객체를 꺼내 암호화된 비밀번호와 getPassword() 비밀번호를 비교하여 일치하면 인증에 성공
'Spring Boot' 카테고리의 다른 글
Spring-Boot JWT Access Token Refresh Token (1) | 2024.12.07 |
---|---|
Spring-Boot AOP개념 @Aspect Advisor (0) | 2024.10.03 |
Spring-Boot @Transactional 트랜잭션 전파 (0) | 2024.09.15 |
Spring Data JPA + Query Dsl JPA (2) | 2024.09.07 |
Spring-Boot H2 데이터베이스 설정과 연결 (1) | 2024.09.02 |