ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SpringBoot] 스프링 시큐리티 적용하기
    Spring 2019. 12. 24. 11:15

    개요

    토이 프로젝트를 진행하면서 사용자 권한 관리 기능을 맡게 되었다. 팀원들과 기획을 하는 중에 스프링 시큐리티를 사용하면 사용자 권한 관리 및 보안 등을 별도의 복잡한 로직 없이 간단한 설정으로 빠르게 구현할 수 있다고 하여 이번 프로젝트에 적용해 보았다.

     

    1. pom.xml

    스프링 시큐리티를 사용하기 위해서는 pom.xml에 아래와 같이 작성해 주어야한다.

            <!-- 스프링 시큐리티 -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-security</artifactId>
    		</dependency>

     

    2. 로그인 페이지

    해당 사용자의 아이디와 비밀번호를 SpringSecurityConfig에서 구분할 수 있게 각각 id값을 셋팅해주었다.

    login.jsp

     

    3. SpringSecurityConfig

    @Configuration
    @EnableWebSecurity
    @EnableGlobalAuthentication
    @ComponentScan(basePackages = {"com.kh.*"})
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    	
    	//로그인과 관련된  Provider 의존성 주입
    	@Autowired
    	AuthProvider authProvider;
    	
    	//로그인 실패시 이동할 Handler 의존성 주입
    	@Autowired
    	LoginFailureHandler authFailureHandler;
    	
    	//로그인 성공시 이동할 Handler 의존성 주입
    	@Autowired
    	LoginSuccessHandler authSuccessHandler;
    	
    	
    	//비밀번호 BCrypt 암호화를 위한 Bean 등록
    	@Bean
        public BCryptPasswordEncoder passwordEncoder() {
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            return bCryptPasswordEncoder;
        }
    
    	//로그인 필요없이 특정 path("/**/*.css", "/**/*.png", "/**/*.jpg", "/**/*.gif")에 접근이 가능하도록 설정
    	@Override
    	public void configure(WebSecurity web) {
    		web.ignoring().antMatchers("/**/*.css", "/**/*.png", "/**/*.jpg", "/**/*.gif");
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    
    		//로그인 설정
    		http.authorizeRequests()
    			//permitAll() 메소드는 어떠한 보안 요구 없이 요청을 허용해준다.
    			.antMatchers("/login", "/login/**", "/users/**", "/error", "/error/**").permitAll()
    			//hasAnyRole() 메소드는 여러개의 권한 설정가능
    			.antMatchers("/common/**").hasAnyRole("AUTH0000", "AUTH0100", "AUTH0101", "AUTH0001")
    			.antMatchers("/seller/**").hasAnyRole("AUTH0000", "AUTH0100")
    			//hasRole() 메소드는 하나의 권한만 설정가능
    			.antMatchers("/admin/**").hasRole("AUTH0000")
    		.and()
    			//csrf 비활성화
    			.csrf().disable()
    			//로그인 폼 사용
    			.formLogin()
    			//로그인 URL 설정 이 설정이 없으면 스프링 시큐리티에서 제공해주는 로그인 페이지로 이동
    			.loginPage("/login")
    			//authProvider에서 로그인 성공시 이동할 Handler 설정
    			.successHandler(authSuccessHandler)
    			//authProvider에서 로그인 실패시 이동할 Handler 설정
    			.failureHandler(authFailureHandler)
    			//해당 URL 요청이 들어오면  authProvider로 로그인 정보를 전달하여 로그인 로직 수행될 수 있도록 설정
    			.loginProcessingUrl("/j_spring_security_check")
    			//이 설정을 통해 사용자가 전달한 key 값이 아이디인지 알 수 있다.(login.jsp 정의) 
    			.usernameParameter("adminId")
    			//이 설정을 통해 사용자가 전달한 key 값이 패스워드인지 알 수 있다.(login.jsp 정의) 
    			.passwordParameter("adminPw")
    		.and()    
    			//로그아웃 관련 설정
    			.logout()
    			//로그아웃을 요청할 URL
    			.logoutUrl("/logout")
    			.invalidateHttpSession(true)
    			//로그아웃 성공시 보낼 페이지 설정
    			.logoutSuccessUrl("/login")
    		.and()
    			//로그인 프로세스가 진행될 provider 설정
    			.authenticationProvider(authProvider);
    	}
    	
    }

    hasAnyRole 부분을 보면 권한 값이 "AUTH0000"로 설정 되어있다.

    하지만 실제 DB에서는 해당 권한 값을 "ROLE_AUTH0000"으로 저장하여 데이터를 관리하였다.

    왜냐하면 해당 권한 값을 비교할때 시큐리티가 자동적으로 "ROLE_"라는 값을 prefix로 가져와 비교하기 때문이다.

    여기서 AUTH0000(관리자), AUTH0100(판매자), AUTH0101(일반 사용자)권한을 지정하였다.

     

    4. AuthProvider

    SpringSecurityConfig 작성을 작성하였으면 아이디 존재 여부 및 패스워드 일치 여부 Check를 진행할 AuthProvider를 아래와 같이 작성해준다.

    @Component
    public class AuthProvider implements AuthenticationProvider{
    	
    	@Autowired 
    	HttpServletRequest request;
    
    	@Autowired
    	LoginService loginService;
    	
    	@Autowired
    	BCryptPasswordEncoder passwordEncoder;
    
    	@Override
    	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    		
    		//로그인 성공시 획득할 권한 리스트
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            //유저 정보 객체
            User userVo = new User();
            //권한 정보 값이 담길 변수
            String authority = null;
        	
        	String id = (String) authentication.getPrincipal();
    		String pw = (String) authentication.getCredentials();
            
            //유저 정보조회
            userVo = loginService.selectUsersById(id);
    
            // 아이디 존재하지 않는 경우
            if(userVo == null) {
            	throw new UsernameNotFoundException("");
            }
            
            //비밀번호 불일치
            if(!passwordEncoder.matches(pw, userVo.getUserPwd())) {
            	throw new InvalidPasswordException("");
            }
            
            authority = userVo.getAuth().getAuthRole();
            
            authorities.add(new SimpleGrantedAuthority(authority));
    			
    		return new UsernamePasswordAuthenticationToken(userVo, null, authorities);
    	} 
    	
    	@Override
        public boolean supports(Class<?> authentication) {
    		return true;
        }
        
    }

    패스워드 일치 여부 비교시 주의할 점은 BCrypt 암호화 방식은 키 값이 계속 바뀌기 때문에 equal 메소드로 비교시 비밀 번호를 맞게 입력해도 계속 false 값을 return하기 때문에 matches라는 메소드를 사용하여 패스워드 일치 여부를 비교해 줄 수 있도록 하였다. 

     

    5. LoginSuccessHandler

    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler{
    
    	@Override
    	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    			Authentication authentication) throws IOException, ServletException {
    		
    		ResultVo resultVo = new ResultVo();
    		ObjectMapper mapper = new ObjectMapper();
    		String jsonInString = null;
    		
    		Object vo = authentication.getPrincipal();
    		
    		User userVo = (User) vo;
    		HttpSession session = request.getSession();	
    		
    		session.setAttribute("sessionUser", userVo.getUserId());
    		session.setAttribute("userNm", userVo.getUserNm());
    		session.setAttribute("userSeq", userVo.getUserSeq());
    		
    		// 권한 체크 후 default URL 세팅
    		String adminAuth = userVo.getAuth().getAuthRole();
    		String homeUrl = null;
    		
    		//로그인 성공시 사용자의 권한별로 homeUrl 설정
    		if (adminAuth.equals("ROLE_AUTH0000") || 
    					adminAuth.equals("ROLE_AUTH0100") || 
    					 adminAuth.equals("ROLE_AUTH0101")) {
    			homeUrl = "/main";
    		}
            
    		session.setAttribute("homeUrl", homeUrl);
    		
    		resultVo.setResult("로그인 성공");
    		resultVo.addResult("homeUrl", request.getContextPath() + homeUrl);
    		jsonInString = mapper.writeValueAsString(resultVo);
    		
    		response.setContentType("application/json");
    		response.setCharacterEncoding("utf-8");
    		PrintWriter out = response.getWriter();
    		
    		out.print(jsonInString);
    		out.flush();
    		out.close();
    	}
    
    }

    AuthProvider에서 로그인이 성공하게되면 SpringSecurityConfig의 설정에 의해 LoginSuccessHandler로 이동하게된다. LoginSuccessHandler에서는 사용자의 Session을 생성해주고 권한별로 이동할 패이지를 설정해 이동할 수 있게 구현하였다.

     

    결론

    기존에 스프링 시큐리트를 사용하기 전에는 사용자의 권한을 Check 부분을 각각의 페이지 별로 설정해 주는 방법을 사용하였기 때문에 권한이 추가되거나 하는 변동 사항이 생기면 각 페이지 별로 수정을 해줘야하는 번거로움이 있었다. 스프링 시큐리티를 적용하니 이러한 번거로운 작업들을 단순하게 구현하여 유저의 인증과 인가 부분을 쉽고 빠르게 구현할 수 있었다. 인증과 인가에 대한 유지보수 측면에서도 편리할 것 같다.

Designed by Tistory.