ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 학원 day98. Spring Security
    기록 2023. 1. 20. 12:52

    spring-web 프로젝트

                * spring framework로 생성한 프로젝트

                * spring bean configuraion 파일 정의

                * web.xml에 spring 관련 설정 추가

                * spring-context, spring-webmvc, spring-jdbc, oracle, mybatis, mybatis-spring 등의 라이브러리 의존성 추가

    springboot-rest 프로젝트

                * spring boot로 생성한 프로젝트 

                * application.yml 설정파일 사용

                * spring-boot-starter-web, mybatis-spring-boot-starter, spring-boot-devtools, ojdbc8, lombok, spring-boot-starter-test 라이브러리 의존성 추가

                * REST API를 구현한 웹 애플리케이션 프로젝트

                            * 사용자정보에 대한 추가, 조회, 변경, 삭제처리를 제공하는 REST API 기반 웹 애플리케이션 프로젝트

                            * 클라이언트와 서버가 json 형식의 데이터를 주고받는다.

                            * 별도의 Front-end 애플리케이션 프로젝트 생성해서 rest api 애플리케이션과 상호작용함  

    springboot-app 프로젝트

                * spring boot로 생성한 프로젝트

                * application.yml 설정파일을 사용

                * spring-boot-starter-web, mybatis-spring-boot-starter, spring-boot-devtools, ojdbc8, lombok, spring-boot-starter-test 라이브러리 의존성 추가

                * spring-boot-starter-security, spring-security-taglib, spring-security-test 라이브러리 의존성 추가

                * tomcat-embed-jasper, jstl 라이브러리 의존성 추가 (jsp 사용시 필요한 라이브러리 의존성)

                * 특징 

                        - 회원가입시 사용자 비밀번호가 암호화되어 저장된다.

                        - 로그인 페이지만 제공하고, 로그인 처리는 spring-security가 수행한다.

                        - 인증되지 않은 사용자는 홈, 회원가입, 로그인만 접근이 허용된다. 

                        - 인증된 사용자는 게시판을 이용할 수 있다. 

                                     * ROLE_GUEST 권한을 가진 사용자는 게시판 목록, 상세정보, 댓글 작성, 댓글 삭제가 가능하다. 

                                     * ROLE_USER 권한을 가진 사용자는 게시판 목록, 상세정보, 게시글 등록, 게시글 삭제, 댓글 작성, 댓글 삭제가 가능하다.

                        - ROLE_USER 권한을 가진 사용자는 정보를 이용할 수 있다.

                                     * 사용자 상세정보, 비밀번호 변경, 탈퇴가 가능하다.

                        - ROLE_ADMIN 권한을 가진 사용자는 관리자 기능을 이용할 수 있다.  

     

    pom.xml
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.6.7</version>
    		<relativePath /> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.example</groupId>
    	<artifactId>springboot-app</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>war</packaging>
    	<name>springboot-app</name>
    	<description>Demo project for Spring Boot</description>
    	<properties>
    		<java.version>11</java.version>
    	</properties>
    	<dependencies>
    		<!-- 
    			spring-security 라이브러리 의존성 추가
    		-->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-security</artifactId>
    		</dependency>
    		<!-- 
    			jsp용 spring-security-tag 라이브러리 의존성 추가
    		-->
    		<dependency>
    			<groupId>org.springframework.security</groupId>
    			<artifactId>spring-security-taglibs</artifactId>
    		</dependency>
    		<!-- 
    			spring-web 라이브러리 의존성 추가
    		-->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<!-- 
    			mybatis 라이브러리 의존성 추가
    		-->
    		<dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>2.3.0</version>
    		</dependency>
    		<!-- 
    			spring-boot devtools 라이브러리 의존성 추가  (코드를 수정하면 서버를 재시작하는 기능) 
    		-->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-devtools</artifactId>
    			<scope>runtime</scope>
    			<optional>true</optional>
    		</dependency>
    		<!-- 
    			oracle jdbc 드라이버 라이브러리 의존성 추가
    		 -->
    		<dependency>
    			<groupId>com.oracle.database.jdbc</groupId>
    			<artifactId>ojdbc8</artifactId>
    			<scope>runtime</scope>
    		</dependency>
    		<!-- 
    			lombok 라이브러리 의존성 추가
    		 -->
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<optional>true</optional>
    		</dependency>
    		<!-- 
    			jsp를 지원하는 내장형 톰캣 라이브러리 의존성 추가
    		 -->
    		<dependency>
    			<groupId>org.apache.tomcat.embed</groupId>
    			<artifactId>tomcat-embed-jasper</artifactId>
    		</dependency>
    		<!-- 
    			jstl 태그 라이브러리 의존성 추가
    		 -->
    		<dependency>
    			<groupId>javax.servlet</groupId>
    			<artifactId>jstl</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.security</groupId>
    			<artifactId>spring-security-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    				<configuration>
    					<excludes>
    						<exclude>
    							<groupId>org.projectlombok</groupId>
    							<artifactId>lombok</artifactId>
    						</exclude>
    					</excludes>
    				</configuration>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>

    스프링부트 2.6.7.버전 사용함. 2.7.7은 스프링 시큐리티가 변경되는 부분이 있음.

    스프링시큐리티 태그

    스프링 시큐리티(Spring Security)

    • 스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한관리, 인가 등)을 담당하는 프레임워크다.
    • 인증(Authentication)과 인가(Authorization)를 담당하는 프레임워크다.
    • 스프링 시큐리티는 서블릿 필터(Filter)와 이들로 구성된 필터체인으로 보안과 관련된 처리를 수행한다.

    스프링 시큐리티의 주요 용어

    • 접근 주체(Principal) : 보호된 리소스에 접근하는 대상(사용자)
    • 인증(Authentication) : 보호된 리소스에 접근하는 대상이 누구인지, 애플리케이션의 작업을 수행해도 되는 주체인지 확인하는 과정
      • 예시) Form 기반 로그인
    • 인가(Authorization) : 해당 리소스에 대한 접근 가능한 권한을 가지고 있는지 확인하는 과정(인증과정 이후에 수행한다.)
    • 권한 : 어떠한 리소스에 대한 접근 제한, 모든 리소스는 접근 제어 권한이 걸려있으며, 인가과정에서 해당 리소스에 대해 제한된 최소한의 권한을 가졌는지 확인한다.

    DelegatingFilterProxy : spring mvc가 실행되기 전에 실행되는 것으로, 클라이언트의 요청을 가로챈다.

    FilterChainProxy에 요청처리를 위임한다. FilterChainProxy에 다양한 필터가 있다. 스프링 컨테이너에 등록되어 있다.

     

    spring security의 실행 흐름

    1. 사용자가 요청을 보낸다.

    2. 웹서버의 필터들이 실행되고, 그중에서 DelegatingFilterProxy가 요청을 받으면, 자신이 받은 요청정보를 포함하고 있는 요청객체를 springSecurityFilterChain으로 요청을 위임한다. (필터를 모두 가진 게 springSecurityFilterChain임)

    3. FilterChainProxy는 자신이 가진 각각의 필터들을 차례대로 수행하면서 보안처리를 수행한다. 

    4. 보안처리가 완료되면 spring mvc로 요청을 전달한다.

    Filter는 서블릿이나 jsp가 실행되기 전에 실행된다.

    springmvc의 첫 진입인 dispatchersevlet이 실행되기 전에 보안처리가 되도록 FilterChainProxy를 끼워놓았다.

    Principal은 사용자, 주체인데 id일수도, email일수도, 사원번호일수도 있다.

    Authorities는 접근 권한이고, Authenticated가 인증여부이다.

    인증이 완료되기 전까지 Authentication은 아이디와 비밀번호만 갖고 있다.

    인증관리자는 AuthenticationProvider를 이용해서 인증작업을 수행하는데 인증이 완료되면 Authentication 객체를 새로 만들고 새로 만들어진 Authentication에는 사용자 정보, 사용자 접근권한, 인증 여부가 들어있다. 

    인증완료 후 새로운 요청이 들어오면 Authentication객체에서 사용자정보와 권한정보를 꺼내온다. 

     

    Authentication 

    - 현재 접근하는 주체(사용자)의 정보와 권한을 표현하는 인터페이스다.

    UsernamePasswordAuthentication Token

    - Authentication을 구현한 객체다.

    - 인증전에는 아이디와 비밀번호정보가 들어있다.

    - 객체를 Authentication Manager에 전달한다. 

    Authentication Manager

    - 등록된 AuthenticationProvider를 통해서 실질적인 인증작업이 수행된다.

    - 인증이 완료되면 Authentication 객체를 SecurityContext에 저장한다. 인증상태를 유지하기 위해 세션에 보관한다. 

    AuthenticationProvider

    - 실제 인증에 대한 부분을 처리한다. (사용자가 적은 pw와 데이터베이스에서 가져온 pw가 동일한지 체크함, 동일한 경우 Authentication 객체를 새로 만들어서 그 안에 사용자정보와 권한정보를 넣어서 AuthenticationManager에게 돌려준다. )

    - 인증전의  Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.

    UserDetailService

    - UserDetails객체를 반환하는 loadUserByUsername(String username) 메소드를 가지고 있다.

    (사용자마다 데이터베이스가 모두 다를 수 있기 때문에 사용자정보를 가져오는 메소드는 UserDetailService라는 인터페이스에 있는 loadUserByUsername이라는 이름의 메소드로 구현해야한다고 정해놓음, 사용자 정보를 조회하는 것을 표준화 해놓았다.)

    - 이 인터페이스를 구현해서 데이터베이스에서 사용자정보와 권한정보를 조회해서 UserDetails객체로 반환한다.

    UserDetails

    - 인증이 완료된  Authentication객체를 생성한 UsernamePassword AuthenticationToken을 생성할 때 사용된다. 

    - UserDetails 객체는 사용자정보와 사용자의 권한정보를 포함하고 있다.

    (사용자에 따라 사용자정보가 user일수도 employee일수도 customer일수도 있기 때문에 사용자 정보를 담는 것도 표준화 해놓았다.)

     

    UserDetails 인터페이스
    LoginUser와 CustomUserDetails

    CustomUserDetails는 LoginUser를 상속받고, UserDetails를 구현한다. 

    CustomUserDetails.java
    
    /**
     * UserDetails 인터페이스를 구현한 클래스다. <br/>
     * 사용자정보(아이디, 비밀정보, 권한정보)를 제공하는 클래스다. <br/>
     * AuthenticationProvider가 사용자 인증을 할 때 이 객체의 정보를 이용한다.
     * AuthenticationProvider은 사용자 인증이 완료되면 Authentication객체를 새로 생성해서, 사용자정보, 권한정보, 인증완료여부를 저장한다.
     * @author NSJ
     *
     */
    public class CustomUserDetails extends LoginUser implements UserDetails {
    
    	private static final long serialVersionUID = -1022370295119655350L;
    	
    	private Collection<? extends GrantedAuthority> authorities;
    	
    	public CustomUserDetails(String userId, String password, String userName, Collection<? extends GrantedAuthority> authorities) {
    		super(userId, password, userName);
    		this.authorities = authorities;
    	}
    
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		return authorities;
    	}
    
    	@Override
    	public String getPassword() {
    		return getEncryptPassword();
    	}
    
    	@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;
    	}
    	
    	
    }

    인증된 사용자정보를 전달받기 위해 LoginUser를 만듦

    HandlerMethodArgumentResolver가 필요없다. @AuthenticatedUser를 붙여주면 된다.

    AuthenticatedUser에서는 @AuthenticationPrincipal를 붙여준다.

    @AuthenticationPrincipal은 Authentication에서 Principal을 달라는 것이다.

    Authentication 객체의 principal에는 CustomerUserDetails 정보가 담겨있다. 

    CustomUserDetailsService.java
    
    /**
     * UserDetailsService 인터페이스를 구현한 클래스다. <br/>
     * UserDetailService 인터페이스의 UserDetails loadUserByUsername(String userId) 메소드를 구현한 클래스다. <br/>
     * loadUserByUsername(String username) 메소드는 사용자명(아이디, 이메일, 사원번호, 학생번호, 직원아이디 등)으로 데이터베이스에서
     * 사용자정보와 권한정보를 조회해서 UserDetails 객체에 담아서 반환하는 메소드다.<br/>
     * UserDetailsService 인터페이스의 구현클래스에 재정의된 loadUserByUsername(String username)메소드가 반환하는 UserDetails 구현객체가 
     * 인증과정에서 사용된다. <br/>
     * 
     * CustomUserDetailService 클래스에서는 사용자정보와 권한정보를 조회해서 CustomUserDetails객체를 생성하고 담아서 반환했다.<br/>
     *
     */
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
    	// 사용자정보와 사용자 권한 정보를 조회하기 위해서 UserMapper와 UserRoleMapper를 주입받는다.
    	@Autowired
    	private UserMapper userMapper;
    	@Autowired
    	private UserRoleMapper userRoleMapper;
    	
    	@Override
    	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
    		// userId는 사용자아이디로 인증직업을 수행하는 AuthenticationProvider로부터 전달받은 것이다.
    		
    		// 사용자아이디로 사용자정보를 조회한다.
    		User user = userMapper.getUserById(userId);
    		// 사용자정보가 존재하지 않으면 예외를 던진다.
    		if (user == null) {
    			throw new UsernameNotFoundException("사용자 정보가 존재하지 않습니다.");
    		}
    		if ("Y".equals(user.getDeleted())) {
    			throw new UsernameNotFoundException("탈퇴한 사용자입니다.");
    		}
    		// 사용자의 권한정보를 조회한다.
    		List<UserRole> userRoles = userRoleMapper.getUserRolesByUserId(userId);
    		// 조회된 권한정보로 GrantedAuthority객체를 생성한다.
    		Collection<? extends GrantedAuthority> authorities = this.getAuthorities(userRoles);
    		
    		return new CustomUserDetails(
    				user.getId(),              // 사용자 아이디
    				user.getEncryptPassword(), // 암호화된 사용자 비밀번호
    				user.getName(),        // 사용자이름
    				authorities);          // 사용자가 보유한 권한정보  
    	}
    	
    	// 사용자 권한정보 목록을 전달받아서 GrantedAuthority객체의 집합으로 반환한다.
    	private Collection<? extends GrantedAuthority> getAuthorities(List<UserRole> userRoles) {
    		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    		
    		for (UserRole userRole : userRoles) {
    			SimpleGrantedAuthority authority = new SimpleGrantedAuthority(userRole.getRoleName());
    			authorities.add(authority);
    		}
    		
    		return authorities;
    	}
    }

    우리가 Controller에서 필요한 건 Authentication객체안에 들어있는 pricipal에 담긴 CustomUserDetails객체이다.

    권한은 어차피 인가과정에서 처리되기 때문에 필요 없음.  

      @AuthenticateUser라는 어노테이션을 붙이면 Authentication의 principal이라는 객체 안에 들어있는 걸 꺼내서 LoginUser에 넣어준다.

    비밀번호 암호화

    매번 다르게 비밀번호가 저장되기 때문에 비밀번호 변경 작업시, matches라는 메소드를 사용해서 기존 비밀번호와 암호화된 비밀번호를 비교할 수 있다. 

     

    CustomSecurityConfig.java
    
    /**
     * spring security 설정정보를 제공하는 클래스다.<br/>
     * @author NSJ
     */
    @EnableWebSecurity
    public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
    
    	// 인증에 필요한 사용자정보와 권한정보를 포함하는 UserDetails객체를 반환하는 CustomUserDetailService객체를 의존성 주입받는다.
    	@Autowired
    	private CustomUserDetailsService customUserDetailsService;
    	// 회원가입시 비밀번호 암호화에 사용했던 비밀번호인코더 객체를 의존성 주입받는다.
    	@Autowired
    	private PasswordEncoder passwordEncoder;
    	
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http
    			.csrf().disable()
    			.authorizeHttpRequests()
    			.antMatchers("/", "/register", "/registered", "/login").permitAll()  // 제시된 요청은 접근을 허용한다. 인증되지 않아도 전부다 접근 허용
    			.antMatchers("/post/**").hasAnyRole("GUEST", "USER")  // 제시된 요청은 ROLE_GUEST 혹은 ROLE_USER 권한이 필요하다.
    			.antMatchers("/user/**").hasRole("USER")    // 제시된 요청은 ROLE_USER 권한이 필요하다. 사용자권한을 가진 사람만 가능, 비밀번호변경하기, 탈퇴하기 등
    			.antMatchers("/admin/**").hasRole("ADMIN") // 제시된 요청은 ROLE_ADMIN 권한이 필요하다. 관리자권한을 가진 사람만 접근 가능
    			.anyRequest().authenticated()   // 위에서 제시된 요청외의 모든 요청도 반드시 인증이 필요하다. 인증된 사용자만 접근 허용
    		.and()
    			.formLogin()                               // 인증방식이 폼인증 방식을 사용하도록 지정한다. FormLoginConfigurer 객체를 반환한다.
    			.loginPage("/login")                       // 로그인 폼을 요청하는 URI를 지정한다. 
    			.loginProcessingUrl("/login")              // 로그인 처리를 요청하는 URI를 지정한다. 
    			.usernameParameter("id")                   // 로그인 폼의 사용자이름 입력필드 이름을 지정한다.
    			.passwordParameter("password")             // 로그인 폼의 비밀번호 입력필드 이름을 지정한다.
    			.defaultSuccessUrl("/")                    // 로그인 성공시 리다이렉션할 URI를 지정한다.
    			.failureUrl("/login?error=fail")           // 로그인 실패했을 경우 재요청할 URI를 지정한다.
    		.and()
    			.logout()								// 로그아웃, LogoutConfigurer객체를 반환한다.
    			.logoutUrl("/logout")                   // 로그아웃 처리를 요청하는 URI를 지정한다.
    			.logoutSuccessUrl("/")                  // 로그아웃 성공시 리다이렉션할 URI를 지정한다.
    		.and()
    			.exceptionHandling()                     // 예외처리, ExceptionHandlingConfigurer 객체를 반환한다.
    			.accessDeniedPage("/access-denied");     // 접근이 거부되었을 때 요청할 URI를 지정한다. 
    	}
    	
    	// 이미지, 스타일시트, 자바스크립트소스와 같은 정적 컨텐츠는 인증/인가 작업을 수행하지 않도록 설정한다.
    	public void configure(WebSecurity web) throws Exception {
    		web.ignoring().antMatchers("/resources/**", "/favicon.ico");
    	}
    	
    	// 사용자정의 CustomUserDetailsService객체와 이 애플리케이션에서 사용하는 비밀번호 인코더를 AuthenticationManager에 등록시킨다.
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth.userDetailsService(customUserDetailsService)
    		.passwordEncoder(passwordEncoder);
    	}
    	
    }

    댓글

Designed by Tistory.