Session 방식으로 SpringSecurity 회원가입-로그인-로그아웃 적용 해보기

조대훈·2024년 3월 26일
0

스프링 시큐리티

목록 보기
1/3
post-thumbnail
post-custom-banner

강의를 보며 학습한 내용을 복습차 다시 정리 하였습니다. 프론트단 코드는 작성 제외 하였습니다. (mustache)

학습 목표
MariaDB, JPA 를 이용해 DB단 연결
Mustache를 이용해 프론트 SSL로 프론트단 간단히 구현
SpringSecurity Session (FormLogin) 방식으로 로그인,로그아웃, 중복 로그인 방지 구현,crsf 토큰

디렉토리 구조

  • Config
    - SecurityConfig
  • Controller
    - AdminController
    - JoinController
    - LoginController
    - LogoutController
    - MainController
  • DTO
    - CustomUserDetails
    - JoinDTO
  • Entity
    - UserEntity
  • Repository
    - UserRepository (interface)
  • Service
    - CustomerUserDetailService
    - JoinService

회원 가입 로직 구현과 기본 디렉토리 구성

@Configuration
@EnableWebSecurity
public class SecurityConfig{

	@Bean
	BCyrptPasswordEncoder bCryptPasswordEncoder(){

	reutrn new BCryptPasswordEncoder();
	}


	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) thorws Exception{

	http
		.authorizeHttpRequest((auth)-> auth
			.requestMatchers("/login","/join","joinProc").permitAll()
			.requestMathcers("/").hasAnyRole("USER")
			.requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
			.anyRequest().authenticated();
		);

	http
		.formLogin((auth)-> auth.loginPage("/login")
			.lgoinProcessingUrl("/loginProc")
			.permitAll()
		);

	http
		.crsf((auth)->auth.disalbe());

	return http.build();

	}

}

SecurityFilterChain 은 인증, 인가 과정을 담당하는 클래스이다. 간략히 말하면 SpringSecurity가 제공하는 필터들의 모음을 어떻게 작동할지 설정하는 부분이다. 아래는 요청이 FilterChain 에 도달하는 대강의 모식도이다.

SecurityFilterChain 메서드를 정의하고 BCyrptPasswordEncoder 는 비밀번호를 암호화 하는데 사용하는 메서드이다. 여담으로 메서드체이닝으로 한 번에 http를

Http 요청 -> WebApplication Server ->필터1 -> 필터2 -> Servlet -> 컨트롤러

참고:https://velog.io/@choidongkuen/Spring-Security-Spring-Security-Filter-Chain-%EC%97%90-%EB%8C%80%ED%95%B4


@Controller
public  class AdminController(){

	@GetMapping("/admin")
	public String adminP(){
	
	return  "admin"
	}

	
}

@Controller
public class  MainController{

	@GetMapping("/")
	public String mainP(){
	return "main"
	}

}

메인 하고 어드민은 바디로 받지 않고 간단히 뷰에 해당 페이지 구분만 해준다


public interface UserRepository extends JPARepository<UserEntity,Integer>

JPA 레퍼지토리를 상속 받는데 첫 번째 파라메터는 받을 객체, 두 번쨰 파라메터는 구분할 아이디의 변수타입명을 참조형으로 넣는다


@Data
@Entity
public class UserEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String role;

}

JoinService

@Service
@AllArgsConsturctor
pbulic class JoinService{
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bcyrptPasswordEncoder;

	public void joinProcess(JoinDto joinDto){

	UserEntity user = new UserEntity;

	user.setUsername(joinDto.getUsername());
		user.setPassword(bCryptPasswordEncoder.ecode(joinDto.getPasword()));

	user.setRole("ADMIN");
	userRepository.save(user);
	}

}

JointDTO 를 통해 username, password 를 get 한뒤 UserEntity에 set 해서 repository에 save 해준다.

JoinDTO

@Data
public class JoinDto{

	private String username;
	private String passwrod;
}

JoinController

@Controller public class JoinController{

	@GetMapping("/join")
	public String joinP(){
	
		return "joinP"
	}

	@PostMapping("/join")
	public String joinProcess(JoinDto joinDto){
		joinservice.joinProcess(joinDto);
	return "redirect:/login";
	}
}

JoinService 중복 검증 과정 추가, DB 기반 로그인 검증 로직 추가

UserEntity

@Entity
public class UserEntity{
	private Integer id;
	private String password;

	@Column(unique=true)
	private String name;
	private String role;
}

UserEntity 내에 @Column(unique=true) 속성 추가

UserRepository


public interface UserRepository extends JPARepository<UserEntity, Integer>{

	boolean existsByUsername(String username);
	UserEntity findByUserName(String username);
}

JoinService

@Service
@AllArgsConsturctor
pbulic class JoinService{
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bcyrptPasswordEncoder;

	public void joinProcess(JoinDto joinDto){


	boolean isUser= userRepository.findByUsername(joinDto.getUsername())

	if(isUser){ retunrn;}

	UserEntity user = new UserEntity;

	user.setUsername(joinDto.getUsername());
		user.setPassword(bCryptPasswordEncoder.ecode(joinDto.getPasword()));

	user.setRole("ADMIN");
	userRepository.save(user);
	}
}

isUser 를 통해 중복 검사 로직 joinService 에 추가

CustomerUserDetailService


@Service
@RequiredArgsConstructor
pulbic CustomerUserDetailService implements UserDetailService{
	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	
		UserEntity userEntity= userRepository.findByusername(username);

		if(userEntity!=null){
			return new CustomerUserDetails(userdata);
		}
	
		return null;
		
	}
	
}

UserDetailService는 DB에서 사용자의 정보를 가져오는 역할을 한다. 보통은 UserDetails로 반환값을 갖는다. 사용하려면 loadByUsername 메서드를 오버라이드 해서 사용해야 하며 이 메서드를 사용하기 위해 레퍼지토리를 주입 받고 미리 구현해놓은 findByUsername을 통해서 userEntity 를 찾은 뒤에 UserDetails 로 반환할 때 UserEntity 를 넣으면 된다.

UserDetail

해당 인터페이스는 사용자의 세부 정보를 제공하는데 권한, 아이디, 비밀번호, 유저 잠금상태, 만료, 사용가능 여부 등 아주 다양한 속성을 담는다. 일단 오버라이드 후 안에 로직들을 천천히 채워나가면 된다.

public class CustomUserDetails implements UserDetails {  
  
  
    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() {  
                return userEntity.getRole();  
            }  
  
        });  
  
        return collection;  
    }  
  
    @Override  
    public String getPassword() {  
        return userEntity.getPassword();  
    }  
  
    @Override  
    public String getUsername() {  
        return userEntity.getUsername();  
    }  
  
    @Override  
    public boolean isAccountNonExpired() {  
        return false;  
    }  
  
    @Override  
    public boolean isAccountNonLocked() {  
        return false;  
    }  
  
    @Override  
    public boolean isCredentialsNonExpired() {  
        return false;  
    }  
  
    @Override  
    public boolean isEnabled() {  
        return false;  
    }  
}

권한 부여 부분외 부분을 유의하고 나머지는 유저 엔티티 주입 후 아이디와 패스워드를 get 하고 그 외 속성들은 전부 true로 설정 해준다.


MainController 에서 사용자 username, role 보이게 변경

@Controller
public class MainController{

	public String mainP(Model model){

	String id = SecurityContextHolader.getContext().getAuthentication().getName();
	
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

Iterator<? extends GrandAuthority> iter = authorities.iterator();

GrandAuthority auth = iter.next();

String role = auth.getAuthoritiy();

	model.addAttribute("id", id);
	model.addAttribute("role",role);

	return "main";

	}
}

getAuthentication() 현재 인증 객체를 가져온다
getAuthorities() 인증 객체에서 권한 정보 컬렉션을 가져온다
iterator() 반복자
next() 반복자를 이용한 첫 번째 권한 정보 객체를 가져온다(여러 권한이 있는 경우를 고려하지 않음)
getAuthority() 권한 정보 객체 이름을 가져온다.

SecurityContextHolder 에서 사용자의 이름을 얻고, getAuthentication() 한 후에 권한을 컬렉션 객체로 가져오는데 여기서 컬렉션으로 가져오는 이유는 객체가 여러 권한을 가지고 있다고 상정하기 때문이다. (권장되는 방식) 예제이므로 두 번째 권한은 표시하지 않는다고 상정한 후 iterater 한후 next() 로 첫 번째 권한만 보여준다


SessionManagement를 이용한 중복 로그인 방지, sessionFixiation 수정

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig{

	@Bean
	BCyrptPasswordEncoder bCryptPasswordEncoder(){

	reutrn new BCryptPasswordEncoder();
	}


	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) thorws Exception{

	http
		.authorizeHttpRequest((auth)-> auth
			.requestMatchers("/login","/join","joinProc").permitAll()
			.requestMathcers("/").hasAnyRole("USER")
			.requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
			.anyRequest().authenticated();
		);

	http
		.formLogin((auth)-> auth.loginPage("/login")
			.lgoinProcessingUrl("/loginProc")
			.permitAll()
		);

//	http.crsf((auth)->auth.disalbe()) 
	// 주석 처리를 하게 되면 enable 하게 되어 로그인시 토큰을 전달 해주어야 한다.
		
	.sessionManagement(
	(auth) -> auth
		.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).sessionFixation((sessionFixation)->sessionFixation.newSession())
		.maximumSession(1)
		.maxSessionPreventsLogin(true)
	)

	http
		.logout((auth)->auth.logoutUrl("/logout")
			.logoutSuccessUrl("/"));

	return http.build();

	}

}

sessionManagement 스프링 시큐리티에서 세션 관리 메서드
세션 생성 정책
sessionCreationPolicy 세션 생성 정책
SessionCreationPolicy.If_REQUIRED 필요한 경우에만 세션을 생성
세션 고정 방지
sessionFixation 세션공격 방지 설정 메서드
newSession 로그인 시 마다 새로운 세션 생성
최대 세션 수
maximumSession 사용자별 최대 세션 수
중복 로그인
maxSessionPreventLogin 메서드는 최대 세션 수 초과시 로그인 차단 여부 설정 true 로그인 차단 false 세션 중 하나를 삭제 후 로그인 허용

sessionManagement 를 통해서 로그인시 매번 새 세션을 발급 받도록 하고 세션은 최대 1개로 정해 중복로그인 방지를 한다. 로그아웃 URL 을 추가했다. crsf 를 enable 했으니 mustache 프론트 단에서

<input type="hidden name="_crsf" value="{{_crsf.token}}" />

위 해당 코드를 추가 해줘야 한다. ( 폼 로그인 코드 안 )

LogoutController


@Controller
public class LogOutController{

	@GetMapping("/logout")
	public String logout(HttpServletRequest request, HttpServletResponse response) throws IOEException{

	Authentication authentication = SecurityContextHolder().getContext().getAuthentication()


if(authentication != null){

	new SecurityContextLogoutHandler().logout(request,response,authentication);
}

return "redirect:/";
	}
}

여기까지 구현 완료. 아래 부터는 부수적인 설명


인메모리 저장 방식

권장되지 않는 방식(DB와 연결되지 않아도 작동하며 주로 작은 프로젝트에서 쓰인다)


@Bean
public UserDetailService userDetailService(){
	UserDetail user1 = User.builder()
		.username("user1")
	.password(bCryptPasswordEncoder().encode("1234"))
	.roles("ADMIN")
	.build();

	return new InMemoryUserDetailManager(user1);
}

builder() 패턴 방식으로 UserDetail 타입 객체를 직접 buidl() 하고 InMemoryUserDetailManager() 에 user을 직접 담아서 return 한다.`


계층 권한 적용 Role_Hierarchy

SecurityConfig

@Bean
public RoleHierarchy roleHierarchy(){

	RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();

hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER\n" + "ROLE_MANAGER > ROLE_USER");

	return hierarchy;
}

가장 높은 계층 권한을 가진 ADMIN은 MANAGER USER 계층 권한에 자연스레 접근 할 수 있다.

  • RoleHierarchyImplRoleHierarchy 인터페이스의 구현체이다.
http
	.authorizeHttpRequest(
	(auth)-> auth

	.requestMatchers("/login","/join").permitAll()
	.requestMatchers("/").hasAnyRole("USER");
	.reqestMatchers("/manager").hasAnyRole("MANAGER");
	.requestMatcher("/admin").hasAnyRole("ADMIN")
	.anyRequest().authenticated()
	)

계층 권한을 구현 해놓으면 hasAnyRole() 부분에 구구절절 권한 목록들을 나열하지 않아도 해당 계층의 위 계층들은 자연스레 접근이 가능 해진다. 구현 해야할 코드가 매우 간단해진다.


총평

sessionstorage를 구현할 코드는 비교적 간단하지만 JWT 토큰을 쓰게 되면 직접 구현해야될 부분들이 더 많아진다. 가령 AutehnticationProvider , Filter UserDetailService TokenUtils JWTUtil 등등.. 다음 포스팅은 JWT를 이용한 기본적인 회원가입 로직을 복습 할 예정이다. ***

profile
백엔드 개발자를 꿈꾸고 있습니다.
post-custom-banner

0개의 댓글