Spring Security -2(인증 객체, config 설정)

EUNJI LEE·2023년 8월 21일
0

Spring

목록 보기
9/13
post-custom-banner

UserDetails 인터페이스를 상속 받은 인증 객체

스프링 시큐리티를 사용하기 위해서 인증된 사용자에 대한 정보를 담을 인증 객체를 만들 때 UserDetails 인터페이스를 상속 받아서 사용한다. 사용할 Entity 혹은 VO객체에 UserDetails 인터페이스를 상속 받아 구현하면 아래 코드와 같다.

💡읽기 전용 객체인 VO객체를 이용해서 중간에 값이 변경되지 않도록 한다.

public class Member implements UserDetails{ //UserDetails를 상속 받음
	private String userId;
	private String password;
	private String name;
	private int age;
	private String email;
	private String gender;
	private String phone;
	private String address;
	private String hobby;
	private Date enrollDate;
	
	@Override //권한 반환
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<GrantedAuthority> auth=new ArrayList<>();
		auth.add(new SimpleGrantedAuthority(MyAuthority.USER.name()));
		if(userId.equals("admin")) {
			auth.add(new SimpleGrantedAuthority(MyAuthority.ADMIN.name()));
		}
		return auth;
	}

	@Override //사용자 id 반환
	public String getUsername() {
		return userId;
	}

	@Override //사용자 pwd 반환
	public String getPassword() {
		return password;
	}

	@Override //계정 만료 여부 반환
	public boolean isAccountNonExpired() {
		//계정 만료 여부를 확인하는 로직 작성
		return true; //true : 만료되지 않음, false : 만료됨
	}

	@Override //계정 잠금 여부 반환
	public boolean isAccountNonLocked() {
		//계정 잠금 여부를 확인하는 로직 작성
		return true; //true : 잠금되지 않음, false : 잠김
	}

	@Override //패스워드 만료 여부 반환
	public boolean isCredentialsNonExpired() {
		// 패스워드 만료 여부를 확인하는 로직 작성
		return true; //true : 만료되지 않음, false : 만료됨
	}

	@Override //계정 사용 가능 여부 반환
	public boolean isEnabled() {
		//계정 사용 가능 여부를 확인하는 로직 작성
		return true; //true : 사용 가능, false : 사용 불가
	}
}

getAuthorities()

사용자가 가지고 있는 권한의 목록을 반환하는 메소드Collection<? extends GrentedAuthority> 타입을 반환 타입으로 갖는다. 위에 작성한 코드로 보면 기본적으로 인증 받으면 USER 권한을 주고 계정의 아이디가 admin인 경우 ADMIN 권한을 같이 줄 수 있도록 작성했다.

getUsername()

사용자를 식별할 아이디를 String 타입으로 반환하는 메소드이다. 이때 사용자로 아이디로 사용하는 값은 사용자를 식별할 값이기 때문에 고유한 값을 사용해야 한다. 보통 PK를 사용하고 아닌 경우라도 유니크 속성이 있는 값을 이용한다.

getPassword()

사용자의 비밀번호를 String 타입으로 반환하는 메소드이다. 저장된 비밀번호는 반드시 암호화 해서 저장해야 한다.

is~()

그 외에 is로 시작하는 메소드는 필요에 따라 사용할 수 있다. 기능 필요에 따라 로직을 구현한 뒤 boolean 타입으로 반환 값을 넘겨주면 된다. 사용 안 하는 경우에는 기본적으로 true를 줘서 사용할 수 있도록 한다.

UserDetailsService 인터페이스를 구현한 Service

스프링 시큐리티에서 로그인을 할 때 사용자 정보를 가져오는 코드를 작성하기 위해서 UserDetailsService 인터페이스를 구현한 service 클래스를 만들어준다.

public class SecurityLoginService implements UserDetailsService{
	@Autowired
	private MemberDao dao;

	//UserDetailsService 인터페이스의 추상 메소드 loadUserByUsername()를 구현
	@Override
	public UserDetails loadUserByUsername(String username)
												throws UsernameNotFoundException {
		//parameter 중에서 username에 작성된 key값을 매개변수로 받아온다.
		Member loginMember=dao.selectMember(session, Map.of("userId",username));
		//DAO에 selectMember() 메소드의 파라미터를 Map타입으로 했기 때문에
		//username을 Map에 담아서 보낸다
		return loginMember;
	}
}

AuthenticationProvider를 구현해서 DB 연동 방식으로 인증

인증 처리 시 인증 방법에 대한 설정을 하는 클래스를 등록해야 하는데 이 때 인메모리 방식과 DB 연동 방식 두가지로 나뉜다. 보통 인메모리 방식을 사용하지 않기 때문에 DB 연동 방식을 이용한다.

@Component
public class DBConnectProvider implements AuthenticationProvider{
	@Autowired
	private MemberDao dao;
	//패스워드 인코더
	private BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		String userId=authentication.getName(); //인증 객체에서 id를 가져옴
		String password=(String)authentication.getCredentials(); //인증 객체에서 비밀번호 가져옴
		
		Member loginMember=dao.selectMemberById(userId);
		if(loginMember==null||!encoder.matches(password, loginMember.getPassword())) {
			//로그인 실패
			throw new BadCredentialsException("인증 실패");
		}
		//인증된 객체를 생성해서 반환
		return new UsernamePasswordAuthenticationToken(
				loginMember, loginMember.getPassword(), loginMember.getAuthorities());
	}

	@Override
	public boolean supports(Class<?> authentication) {
		//UsernamePasswordAhthenticationToken을 사용할 수 있게 함
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
	}
	
}

getName() 메소드는 String 타입을 반환하지만 getCredentials() 메소드는 Object 타입을 반환하므로 형변환해서 String 타입으로 패스워드를 저장해서 사용했다.

BCryptPasswordEncoder.matches()

BCryptPasswordEncoder가 제공하는 matches() 메소드를 이용해서 사용자가 입력한 비밀번호와 DB에 저장된 비밀번호가 일치하는지 확인한다. 일치하면 true를 반환하고 일치하지 않으면 false를 반환한다. 따로 디코딩 할 수 없다.

💡 matches() 메소드 사용 시 첫 번째 인수 값으로 평문으로 된 사용자가 입력한 패스워드 값을 넣고 두 번째 인수로 암호화된 비밀번호를 작성해줘야 한다.

SecurityConfig로 시큐리티 설정

인증 처리를 위한 인증 객체와 서비스가 완성되었으면 실제로 인증 처리를 진행하는 시큐리티 설정 파일을 작성한다. 보통 같은 패키지 내에 config 패키지를 만들어서 클래스 파일을 새로 만들어서 작성한다.

스프링 부트에서 security를 적용하기 위해서는 우선 인증 처리할 빈을 등록하게 되는데 이 역할을 하는 빈을 SecurityConfig의 메소드로 지정한다. 해당 빈은 SecurityFilterChain 객체를 반환한다.

@Configuration //config 클래스로 등록
@EnableWebSecurity //security 설정
public class SecurityConfig {
	@Autowired
	private DBConnectProvider provider;
	
	@Bean
	public SecurityFilterChain authenticationPath(HttpSecurity http) throws Exception {
		//HttpSecurity를 셋팅해서 SecurityFilterChain을 반환
		return http.csrf().disable() //csrf 비활성화
				//토큰을 사용하는 방식이기 때문에 csrf를 비활성화 헤야한.
				.formLogin() //로그인 폼 관련 설정
					.successForwardUrl("/successLogin") //성공했을 때 실행할 url 주소
					.failureForwardUrl("/errorLogin") //실패 시 실행할 url 주소
					.passwordParameter("pw") //패스워드의 파라미터 이름
					.loginProcessingUrl("/login.do") //로그인 처리할 url
					.loginPage("/loginpage") //로그인 페이지 url
				.and()
			//and()메소드를 사용하면 다시 HttpSecurity를 반환해서 접근해서 이어서 사용할 수 있다.
				.authorizeHttpRequests() //인증, 인가 관련 설정
					.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
					.antMatchers("/loginpage").permitAll()
					.antMatchers("/errorLogin").permitAll()
					//loginpage, errorLogin는 권한이 없어도 사용할 수 있게 함
					.antMatchers("/**").hasAnyAuthority(MyAuthority.USER.name())
					//그 외 모든 url 주소에 대해서 USER 권한이 있어야 사용할 수 있게 함
				.and()
				.logout() //로그아웃 관련 설정
					.logoutSuccessUrl("/logout")
					.logoutUrl("/logout.do")
				.and()
				.authenticationProvider(provider)
				.and()
				.exceptionHandling()//에러 발생 시 핸들링 처리
					.authenticationEntryPoint(new AuthenticationEntryPoint() {
						@Override //로그인 안 하고 접근하는 경우
						public void commence(HttpServletRequest request, HttpServletResponse response,
								AuthenticationException authException) throws IOException, ServletException {
							response.sendRedirect(request.getContextPath()+"/error/login");
						}
					}).accessDeniedHandler(new AccessDeniedHandler() {
						@Override //권한이 없는 페이지 접근하는 경우
						public void handle(HttpServletRequest request, HttpServletResponse response,
								AccessDeniedException accessDeniedException) throws IOException, ServletException {
							response.sendRedirect(request.getContextPath()+"/error/auth");
						}
					})
				.build();
	}
}

.formLogin()

form 기반으로 로그인할 경우 필요한 설정에 대해서 작성할 수 있다. FormLoginConfigurer를 불러온다.

successForwardUrl() : 로그인 성공 시 실행할 url 주소를 인수로 작성한다.

failureForwardUrl() : 로그인 실패 시 실행할 url 주소를 인수로 작성한다.

passwordParameter() : password로 사용할 파라미터의 key값을 작성한다.

loginProcessingUrl() : 로그인 처리 시 별도로 실행할 로직이 있을 경우 작성한다.

loginPage() : 시큐리티가 제공하는 로그인 페이지 외에 별도로 만든 로그인 페이지를 이용할 때 사용한다.

authorizeHttpRequests()

특정 HTTP 요청에 대한 엑세스 설정을 할 때 사용한다. 각 설정은 아래와 같다.

요청 구분

requestMatchers() : 인수로 다수의 요청 주소를 작성할 수 있다. 작성한 인수들의 요청에 동일한 권한 제한을 두고 싶을 때 사용할 수 있다.

andMatchers() : 인수로 작성한 url 요청에 대한 설정을 한다.

anyRequest() : 설정한 경로 외에 모든 경로에 대한 권한을 지정한다.

허가 구분

hasRole() : 해당 Role(역할)을 가지고 있는 경우 요청 가능하도록 허용한다.

hasAnyRole() : 인수로 작성한 Role 중 하나라도 가지고 있으면 허용한다.

permitAll() : 해당 요청에 별다른 인증 없이 이용 가능하도록 한다.(무조건 true를 반환)

denyAll() : 어떤 권한 상태라도 무조건 허용하지 않을 때 사용한다.(무조건 false를 반환)

hasAnyAuthority() : 해당 요청에 인수로 작성한 권한이 있어야 이용 접근 가능하게 한다.

isAnonymous() : 익명 사용자를 허용한다.

isRememberMe() : Remember Me 인증을 통해 인증된 계정인 경우 허용한다.

isFullyAuthenticated() : Remember Me 인증이 아닌 일반 인증인 경우 허용한다.

isAuthenticated() : 이미 인증 받은 사용자인 경우 허용한다.

exceptionHandling()

인증, 인가 설정 후 허용되지 않은 요청에 접근하려고 했을 때 예외가 발생하는데 이 때 핸들러를 이용해서 예외 발생 시 처리할 로직을 작성할 수 있다.

authenticationEntryPoint()

로그인 후 접근 가능한 url에 요청을 보냈을 경우 실행되는 메소드로 AuthenticationEntryPoint 객체를 생성 해서 해당 객체 안에 commence() 메소드를 오버라이딩 한다.

@Override //로그인 안 하고 접근하는 경우
public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) throws IOException, ServletException {
		response.sendRedirect(request.getContextPath()+"/error/login");
}

위 코드에서는 요청을 sendRedirect()로 넘겨서 내가 지정한 로그인 에러 url로 응답시켰다.

accessDeniedHandler()

접근하려고 하는 url에 인증된 객체가 필요한 권한이 없는 경우 실행되는 메소드로 AccessDeniedHandler 객체를 생성해서 handle() 메소드를 오버라이딩 한다.

@Override //권한이 없는 페이지 접근하는 경우
public void handle(HttpServletRequest request, HttpServletResponse response,
	AccessDeniedException accessDeniedException) throws IOException, ServletException {
	response.sendRedirect(request.getContextPath()+"/error/auth");
}

위 코드에서는 권한 불일치 예외 발생 시 sendRedirect()로 지정한 권한 에러 url로 응답시켰다.

profile
천천히 기록해보는 비비로그
post-custom-banner

0개의 댓글