수업내용/프로젝트

[Spring/Security] 사용자/관리자 로그인 따로 처리하기 (시큐리티 커스텀)

주니어주니 2023. 2. 13. 18:08

 

 

* 시큐리티 인증 절차 

 

 

 

* 시큐리티 흐름 

 

 

* 로그인 과정

 

 

 

로그인 폼 요청 경로는 따로, 

로그인 처리는 한 곳에서 ("/login") 

 

 

 

0. 로그인폼

 

- action="/login"

- 히든필드에 userType 담아서 보내기 

- error=fail 일 때 문구 넣기

 

* user의 login-form 

 

 

* employee의 login-form 

 

 

 

 

1. security.vo

 

* LoginUser 객체 

package com.example.security.vo;

import com.example.vo.User;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginUser {

	private String id;
	private String nickname;
	private String providerType;
	private String email;
	
	public LoginUser() {}
	
	public LoginUser(User user) {
		this.id = user.getId();
		this.nickname = user.getName();
		this.providerType = user.getProviderType();
		this.email = user.getEmail();
	}
	
}

 

* CustomUserDetails 객체 

- CustomUserDetailsService 객체에서 로그인한 사용자 아이디로 사용자 정보를 조회하고, 그 정보를 담아서 반환하는 객체 

 

package com.example.security.vo;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class CustomUserDetails extends LoginUser implements UserDetails{

	private static final long serialVersionUID = -692518506074118282L;
	
	// db에서 조회한 비밀번호
	private final String encryptPassword;
	private final Collection<? extends GrantedAuthority> authorities;
	
	// 로그인한 id로 사용자/직원 정보를 조회해서 UserDetails 객체에 로그인한 사용자의 아이디, 비밀번호, 이름, 권한정보를 저장
	public CustomUserDetails(String id, String encryptPassword, String name, Collection<? extends GrantedAuthority> authorities) {
		setId(id);
		setName(name);
		this.encryptPassword = encryptPassword;
		this.authorities = authorities;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return encryptPassword;
	}

	@Override
	public String getUsername() {
		return getId();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	
}

 

 

 

2. security.service

- CustomUserDetailsService와 CustomEmployeeDetailsService로 구분!! 

 

* CustomUserDetailsService (사용자 DB 조회)

- User의 Role은 "ROLE_USER" 1개 

 

package com.example.security.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.example.mapper.UserMapper;
import com.example.mapper.UserRoleMapper;
import com.example.security.vo.CustomUserDetails;
import com.example.vo.User;
import com.example.vo.UserRole;

@Service
public class CustomUserDetailsService implements UserDetailsService{

	@Autowired
	UserMapper userMapper;
	@Autowired
	UserRoleMapper userRoleMapper;
	
	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		
		User user = userMapper.getUserById(userId);
		if(user == null) {
			throw new UsernameNotFoundException("사용자 정보가 존재하지 않습니다.");
		}
		if("Y".equals(user.getDeleted())) {
			throw new UsernameNotFoundException("탈퇴한 사용자입니다.");
		}
		
		UserRole userRole = userRoleMapper.getUserRoleByNo(user.getNo());
		
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		authorities.add(new SimpleGrantedAuthority(userRole.getRoleName()));
		
		return new CustomUserDetails(
					user.getId(), 
					user.getEncryptPassword(), 
					user.getName(), 
					authorities);
	}	
}

 

 

* CustomEmployeeDetailsServices (관리자 DB 조회)

- Employee의 권한은 "ROLE_EMP", "ROLE_ADMIN" 

 

package com.example.security.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.example.mapper.EmployeeMapper;
import com.example.mapper.EmployeeRoleMapper;
import com.example.security.vo.CustomUserDetails;
import com.example.vo.Employee;
import com.example.vo.EmployeeRole;

@Service
public class CustomEmployeeDetailsService implements UserDetailsService{

	@Autowired
	EmployeeMapper employeeMapper;
	@Autowired
	EmployeeRoleMapper employeeRoleMapper;
	
	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		// 로그인하면서 전달받은 직원 아이디로 직원 정보 조회
		Employee employee = employeeMapper.getEmployeeById(userId);
		if(employee == null) {
			throw new UsernameNotFoundException("직원 정보가 존재하지 않습니다.");
		}
		// 직원의 권한정보 조회 
		List<EmployeeRole> empRoles = employeeRoleMapper.getEmployeeRolesByEmployeeId(employee.getId());
		
		// 직원의 권한정보 목록을 전달받아서 GrantedAutority 객체의 집합으로 반환
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		
		for(EmployeeRole empRole : empRoles) {
			authorities.add(new SimpleGrantedAuthority(empRole.getRoleName()));
		}
		
		return new CustomUserDetails(
				employee.getId(),
				employee.getPassword(),
				employee.getName(), 
				authorities);
	}
}

 

 

 

 

3. security

- 로그인 단계에서 userType이 추가됐기 때문에 원래 시큐리티를 상속받아서 Custom으로 다시 만들어줌

 

* CustomAuthenticationToken 

package com.example.security;

import java.util.Collection;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class CustomAuthenticationToken extends UsernamePasswordAuthenticationToken{

	private static final long serialVersionUID = 893311451286366129L;

	private String userType;

	public CustomAuthenticationToken(Object principal, Object credentials) {
		super(principal, credentials);
	}
	
	public CustomAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
		super(principal, credentials, authorities);
	}

	public String getUserType() {
		return userType;
	}

	public void setUserType(String userType) {
		this.userType = userType;
	}

}

 

 

* CustomAuthenticationFilter

package com.example.security;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter{

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if(!request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		
		String id = request.getParameter("id");
		String password = request.getParameter("password");
		String userType = request.getParameter("userType");
		
		System.out.println("id: " + id);
		System.out.println("password: " + password);
		System.out.println("userType: " + userType);
		
		CustomAuthenticationToken authenticationToken = new CustomAuthenticationToken(id, password);
		authenticationToken.setUserType(userType);
		
		return this.getAuthenticationManager().authenticate(authenticationToken);
	}
}

 

 

* CustomAuthenticationProvider

- 실질적인 인증 처리 담당 

- map 객체와 비밀번호 인코더 2가지를 전달받음

- 로그인 전의 Authentication 객체에 아이디, 비번 등의 정보를 담고

  로그인 후 Authentication 객체를 새로 만들어서 거기에 userType담아서 Authentication 객체 반환

- CustomAuthenticationToken은 Authentication 인터페이스를 구현하고 있는 구현객체 !!! 그래서 Authentication 타입으로 캐스팅 할 수 있음 

 

package com.example.security;

import java.util.Map;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

// 실제 인증 처리 (인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환) 
// 사용자 타입이 추가됐기 때문에 Custom 정의 
public class CustomAuthenticationProvider implements AuthenticationProvider {

	// detailsServiceMap을 SecurityConfig로부터 전달받음 -> {"user", CustomUserDetailsService, "employee", CustomEmployeeDetailsService}
	private Map<String, UserDetailsService> detailsServiceMap;
	private PasswordEncoder passwordEncoder;
	
	public CustomAuthenticationProvider(Map<String, UserDetailsService> detailsServiceMap, PasswordEncoder passwordEncoder) {
		this.detailsServiceMap = detailsServiceMap;
		this.passwordEncoder = passwordEncoder;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 로그인 전의 Authentication 객체에 로그인 정보 담기 
		CustomAuthenticationToken customAuthentication = (CustomAuthenticationToken) authentication;
		// 로그인 창에서 입력하는 아이디, 비밀번호, 유저타입(히든필드) 저장 
		String username = customAuthentication.getName();
		String password = customAuthentication.getCredentials().toString();
		String userType = customAuthentication.getUserType();
		
		// userType에 해당하는 CustomXXXDetailsService 객체를 Map 객체에서 꺼낸다. 
		UserDetailsService detailsService = detailsServiceMap.get(userType);
		
		// userType에 해당하는 CustomXXXDetailsService 객체를 이용해서 인증에 필요한 사용자정보(UserDetails를 구현한 CustomUserDetails 객체)를 획득한다.
		UserDetails userDetails = detailsService.loadUserByUsername(username);
		// 비밀번호를 비교해서 사용자 인증을 수행한다.
		if(!passwordEncoder.matches(password, userDetails.getPassword())) {
			throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
		}
		
		// 인증이 완료되면 새로운 Authentication 객체 생성해서 사용자정보, 비밀번호, 권한정보를 담는다
		customAuthentication = new CustomAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
		// 사용자 타입을 담는다
		customAuthentication.setUserType(userType);
		
		
		// 반환된 Authentication 객체는 SecurityContext 에 저장되고, SecurityContext 는 HttpSession에 저장된다.
		return customAuthentication;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return CustomAuthenticationToken.class.isAssignableFrom(authentication);
				
	}
}

 

 

* SecurityConfig

- 커스텀한 필터 추가

- 커스텀한 provider를 빈에 등록

- 로그인 성공 후 이동할 경로 각자 설정 (사용자 홈 / 관리자 홈)

package com.example.security;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import com.example.security.service.CustomEmployeeDetailsService;
import com.example.security.service.CustomUserDetailsService;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Autowired
	PasswordEncoder passwordEncoder;
	@Autowired
	CustomUserDetailsService userDetailsService;
	@Autowired
	CustomEmployeeDetailsService employeeDetailsService;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf().disable()
			// 인가정책 설정
			.authorizeRequests()
			// 다음 페이지에 모두 접근 가능
			.antMatchers("/").permitAll()			
			.antMatchers("/user/register", "/user/registered").permitAll()
			.antMatchers("/user/login", "/emp/login").permitAll()
			// 인증된 사람만 다음 기능 가능 
			.antMatchers("/logout").authenticated()
			// 특정 권한을 가진 사람만 다음 페이지 접근 가능
			.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
			.antMatchers("/emp/**").hasAnyRole("EMP", "ADMIN")
			.antMatchers("/admin/**").hasAnyRole("ADMIN")
			.anyRequest().authenticated()
		.and()
			// 인증정책 설정
			.formLogin()							// 인증방식은 폼인증 방식 사용
			.loginProcessingUrl("/login")			// 로그인 처리를 요청하는 URI
		.and()
			// 로그아웃 정책 설정
			.logout()
			.logoutUrl("/logout")					// 로그아웃 처리를 요청하는 URI
			.logoutSuccessUrl("/")					// 사용자 로그아웃 성공 시 재요청할 URI
		.and()
			.addFilter(authenticationFilter())
			.exceptionHandling()
			.accessDeniedHandler(accessDeniedHandler())
		.and()
			.headers().frameOptions().disable();
	}
	
	// 보안정책을 적용하지 않을 URI 설정 (이미지, 스타일시트, 자바스크립트 소스와 같은 정적 콘텐츠는 인증/인가 작업을 수행하지 않도록 설정)
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/resources/**", "/favicon.ico");
	}
	
	// 사용자정의 UserDetailsService 객체(customUserDetailsService)와 비밀번호 암호화객체를 AuthenticationManagerBuilder에 등록
	// -> 기본제공하는거 말고 얘네를 써라
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(authenticationProvider());
	}
	
	@Bean
	public AccessDeniedHandler accessDeniedHandler() {
		return new AccessDeniedHandler() {
			
			@Override
			public void handle(HttpServletRequest request, HttpServletResponse response,
					AccessDeniedException accessDeniedException) throws IOException, ServletException {
				response.sendRedirect("/access-denied");
			}
		};
	}
	
	@Bean
	public CustomAuthenticationFilter authenticationFilter() throws Exception {
		 CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
		 filter.setAuthenticationManager(authenticationManager());
		 filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
		 filter.setAuthenticationFailureHandler(authenticationFailureHandler());
		 return filter;
	}
	
	@Bean
	public AuthenticationProvider authenticationProvider() {
		// 여기서 map에 담아서 CustomAuthenticationProvider에서 값 꺼내서 사용
		Map<String, UserDetailsService> map = new HashMap<>();
		map.put("사용자", userDetailsService);
		map.put("관리자", employeeDetailsService);
		
		return new CustomAuthenticationProvider(map, passwordEncoder);
	}
	
	@Bean
	public AuthenticationSuccessHandler authenticationSuccessHandler() {
		return new AuthenticationSuccessHandler() {
			
			@Override
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				CustomAuthenticationToken customAuthenticationToken = (CustomAuthenticationToken) authentication;
				
				String userType = customAuthenticationToken.getUserType();
				if("사용자".equals(userType)) {
					response.sendRedirect("/");
				} else if("관리자".equals(userType)) {
					response.sendRedirect("/emp/home");
				}
			}
		};
	}
	
	@Bean
	public AuthenticationFailureHandler authenticationFailureHandler() {
		return new AuthenticationFailureHandler() {
			
			@Override
			public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
					AuthenticationException exception) throws IOException, ServletException {
				
				String userType = request.getParameter("userType");
				if("사용자".equals(userType)) {
					response.sendRedirect("/user/login?error=fail");
				} else if("관리자".equals(userType)) {
					response.sendRedirect("/emp/login?error=fail");
				}
			}
		};
	}
}

 

 

 

 

사용자 로그인 창

 

사용자 로그인 후

 

관리자 로그인 창

 

관리자 로그인 후 화면