유튜브 채널 "메타코딩"님의 스프링 부트 시큐리티를 보고 배운 내용을 정리하였습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
회원가입 기능은 일반적인 DB에 저장하는 회원가입과 크게 다르지 않다.
하지만 암호화 부분은 조금 다르다.
Jpa를 사용하여 유저 정보를 관리한다.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role;
@CreationTimestamp
private Timestamp createDate;
}
이 부분은 내가 JPA를 공부하지 않아서 조금 헤메었다.
특이하게 interface를 @Repository를 붙여서 사용한다.
JpaRepository를 상속받아서 구현하는데, 기본적이 CRUD기능이 들어있다.
아래의 메서드는 로그인 때 사용할 메서드를 추가시켰다.
@Repository
public interface UserRepository extends JpaRepository<User,Integer> {
//select * from user where username = ?
public User findByUsername(String username); //Jpa Query Method
}
다른 부분은 특이할 것이 없고, 특이한 점은 암호화 부분이다.
여기서는 BCryptPasswordEncoder를 주입 받아서 사용했다.
(Configuration에서 @Bean등록 예정)
@Controller
public class IndexController {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
//...생략...
@PostMapping("join")
public String join(User user){
System.out.println(user);
user.setRole("ROLE_USER");
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user);
return "redirect:/loginForm";
}
@GetMapping("/join")
public String join(){
return "join";
}
}
오늘의 핵심이 이곳에 담겨있다.
기본적으로 name에 username
password
로 되어있어야 spring security가 잘 불러와서 인증을 진행한다. 하지만 다른것으로 할경우 conriguration에서 .usernameParameter()
과 같이 따로 설정해주어야한다.
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username"/><br/>
<input type="password" name="password" placeholder="Password"/><br/>
<button>로그인</button>
</form>
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.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder encoderPwd() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.csrf().disable();
http.authorizeHttpRequests()
.requestMatchers("/user/**").authenticated() //인증만 되면 들어갈 수 있는 주소
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
//hasAnyRole : 역할중 하나 가지고 있어야 허용
.requestMatchers("/admin/**").hasRole("ADMIN")// 이 역할 필요
.anyRequest().permitAll() //모두 허용
.and() //return HttpSecurity
.formLogin() //return FormLoginConfigurer <HttpSecurity>
//.usernameParameter("username") 기본값은 username임
.loginPage("/loginForm") //로그인 페이지 지정(Controller)
.loginProcessingUrl("/login")
//이 Url이 오면 가로채서 UserDetailsService의 loadUserByname 메서드로 연결
.defaultSuccessUrl("/");
//로그인 성공시 url
return http.build();
}
}
spring security를 설정하는 부분이다.
강의에서는 아래와 같이 설정했다.
public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception{ //... } }
하지만 WebSecurityConfigurerAdapter는 @deprecate되었고 현재는 사라졌다.
그래서 위와 같이 @Bean으로서 SecurityFilterChain을 return하는 메서드를 등록하여서 사용하여야한다.
링크 참조 : https://devlog-wjdrbs96.tistory.com/434
핵심은 HttpSecurity
이다. 전형적인 Builder이다.
.build를 하면 SecurityFilterChain을 뱉어낸다.
이것은 특정한 Http요청에 대한 설정을 제공한다. 기본적으로는 모든 요청에 적용되나, requestMatcher(RequestMatcher)
로 대상을 제한 할 수 있다.
만약 허용되지 않은 유저가 접근시 로그인 페이지로 보낸다.
requestMatcher
: 특정 요청에 대해 설정
authenticated
: 인증된 사용자는 접속허용
hasAnyRole
, hasRole
: 특정 역할이 있어야 접속허용 (여러개 한개 차이)
anyRequest
: 나머지 모든 요청에 대한 설정
permitAll
: 모두 접속 허용
and
: 다시 HttpSecurity를 반환
formLogin
: 로그인 설정
loginPage
: 로그인 페이지 지정 (controller 매핑한 주소)
loginProcessionUrl
: 이 Url이 오면 가로채서 UserDetailsService
를 구현한 @Bean의 loadUserByname 메서드로 보낸다.
defaltSuccessUrl
: 로그인 성공시 갈 주소
usernameParameter
: form 태그에서 전송시 input 태그 이름. 기본값은 "username"
즉 이 설정은 다음과 같은 기능이 있다.
1. 로그인 url이 오면 가로채서 설정한 로그인 페이지로 보냄
2. 로그인 요청이 전송되면(loginProcessionUrl 에 설정한 주소로) UserDetailService
의 loadUserByname
메서드로 보낸다.
3. 요청별로 권한을 확인하고, 권한이 없으면 로그인 페이지로 보낸다.
자 그러면 로그인 권한을 체크하는 UserDetailService
를 살펴보자.
UserDetailsService
인터페이스를 구현한 클래스 이다.
로그인 요청이 들어오면 이 인터페이스를 구현한 클래스로 연결된다.
아래를 보면 인터페이스를 구현하면서 loadUserByname
메서드를 구현하였다.
import com.cos.security1.model.User;
import com.cos.security1.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
// 시큐리티 설정에서 .loginProcessingUrl("/login") 요청이 오면
// 자동으로 UserDetailsService 타입으로 되어있는 loadUserByUsername 메서드 실행
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("username : " + username);
User userEntity = userRepository.findByUsername(username);
if(userEntity != null){
return new PrincipalDetail(userEntity);
}
return null;
}
}
이 메서드는 반환 타입이 UserDetails
이다. (인터페이스)
이는 바로 다음 나온다. Db에서 User 객체를 뽑아서 구현체인 PrincipalDetails
의 생성자에 넣어준다.
이 클래스는 위에서 말했다싶이 UserDetails의 구현체이다.
import com.cos.security1.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
// 시큐리티가 /login 요청이 오면 낚아채서 로그인 진행
// session <= Authentication 객체 <= UserDetails(interface) <implement= 이 객체
// PrincipalDetailsService 는 Au
public class PrincipalDetail implements UserDetails {
private User user;
public PrincipalDetail(User user) {
this.user = user;
}
//사용자에서 부여된 권한을 반환한다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() { //GrantedAuthority를 Collection안에 담기
@Override
public String getAuthority() {
return user.getRole(); //여기서 역할 뽑기
}
});
return collect;
}
//비밀번호 가져오기
@Override
public String getPassword() {
return user.getPassword();
}
//유저 이름 가져오기
@Override
public String getUsername() {
return user.getUsername();
}
//계정만료 안됨?
@Override
public boolean isAccountNonExpired() {
return true;
}
//계정 안 잠김?
@Override
public boolean isAccountNonLocked() {
return true;
}
//계정 Credential 안 만료됨?
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//비활성화 되지 않음? (오랫동안 사용하지 않은 경우 등)
@Override
public boolean isEnabled() {
return true;
}
}
security에서 관리하는 session에는 유저 정보가 담겨있어야 되는데 다음과 같이 층층이 감싸져있다.
session - Authentication - UserDetails(PrincipalDetail)
이거를 구현한 것이다.
호출되는 순서를 쉽게 생각해보면
1. 잘못된 접근
2. .loginPage("/loginForm") -> /loginForm
3. 작성후 /login POST
4. .loginProcessingUrl("/login") 이 가로채서 UserDetailsService
구현체의 loadUserByusername
메서드로 보냄
5. 이곳에서 new PrincipalDetailService() (UserDetailsService 구현체)을 리턴
6. PrincipalDetialsService가 PrincipalDetail 호출