본문으로 바로가기

Spring Security 정리

category PROJECT/TOOL 2019. 11. 12. 15:11

https://dev-elop.tistory.com/43

 

Spring Boot 기본 설정 정리(코드포함)

https://okky.kr/article/364038 OKKY | spring boot 정리 (locale, messageSource, security, jpa, hibernate, Scheduler, config) 안녕하세요 Spring boot 정리 해봅니다. https://github.com/visualkhh/lib-spr..

dev-elop.tistory.com

위 내용 보고 스프링 시큐리티를 스프링 BOOT 에 적용은 했으나 사실 위 부분은 너무 광범위하게 되어 있는 소스라서

간소화해서 적용 한 부분을 정리해본다.

1. DB 테이블 생성 (user)

CREATE TABLE `user` (
	`userSeq` INT(11) NOT NULL AUTO_INCREMENT,
	`userId` VARCHAR(15) NOT NULL COMMENT '사용자아이디',
	`userName` VARCHAR(15) NOT NULL COMMENT '사용자이름',
	`userPassword` VARCHAR(500) NOT NULL COMMENT '사용자비밀번호',
	`accountNonExpired` TINYINT(1) NULL DEFAULT NULL,
	`accountNonLocked` TINYINT(1) NULL DEFAULT NULL,
	`credentialsNonExpired` TINYINT(1) NULL DEFAULT NULL,
	`enabled` TINYINT(1) NULL DEFAULT NULL,
	PRIMARY KEY (`userSeq`),
	UNIQUE INDEX `UNIQUE` (`userId`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=5
;

1-2. DB 테이블 생성 (authority)

CREATE TABLE `authority` (
	`userSeq` INT(11) NOT NULL,
	`userId` VARCHAR(15) NOT NULL,
	`authority` VARCHAR(20) NOT NULL,
	INDEX `FK_authority_user_seq` (`userSeq`),
	INDEX `FK_authority_user_id` (`userId`),
	CONSTRAINT `FK_authority_user_id` FOREIGN KEY (`userId`) REFERENCES `user` (`userId`) ON UPDATE CASCADE ON DELETE CASCADE,
	CONSTRAINT `FK_authority_user_seq` FOREIGN KEY (`userSeq`) REFERENCES `user` (`userSeq`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

1-3. 테이블 설명

해당 테이블은 단순 '사용자'와 '권한' 만 존재한다. 그 외 그룹이나 ROLE 설정을 배재했다. 

accountNonExpired, accountNonLocked, credentialsNonExpired, enabled 값은 모두 1로 설정

2. Entity 소스 생성

import java.io.Serializable;
import java.util.Collection;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.OneToMany;
import javax.persistence.Table;

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

import lombok.Data;

@Entity
@Table(name = "user")
@Data
public class LoginUserDetails implements UserDetails, Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = -6204260175392482439L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="userSeq")
	private Integer userSeq;
	
	@Column(length =  15, nullable = false, unique = true)
	private String userId;
	
	@Column(length =  15, nullable = false)
	private String userName;
	
	@Column(length =  500, nullable = false)
	private String userPassword;
	
	@Column(name="accountNonExpired")
	private boolean accountNonExpired;
	
	@Column(name="accountNonLocked")
	private boolean accountNonLocked;
	
	@Column(name="credentialsNonExpired")
	private boolean credentialsNonExpired;
	
	@Column(name="enabled")
	private boolean enabled;
	

	@OneToMany(fetch = FetchType.EAGER)
	@JoinColumns({
		@JoinColumn(name="userSeq", referencedColumnName = "userSeq"),
		@JoinColumn(name="userId", referencedColumnName = "userId")
	})
	private Collection<LoginUserAuthority> authorities;


	@Override
	public String getPassword() {
		return this.userPassword;
	}


	@Override
	public String getUsername() {
		return this.userName;
	}
}
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.springframework.security.core.GrantedAuthority;

import lombok.Data;

@Data
@Entity
@Table(name = "authority")
public class LoginUserAuthority implements GrantedAuthority{
	/**
	 * 
	 */
	private static final long serialVersionUID = -6452605573888242939L;

	@Id
	@Column(nullable = false)
	private Integer userSeq;
	
	@Id
	@Column(length =  15, nullable = false)
	private String userId;
	
	@Column(length =  20, nullable = false)
	private String authority = null;
	
	public LoginUserAuthority() {
	}
	
	public LoginUserAuthority(String authority) {
		this.authority = authority;
	}
}

3. config 소스 생성

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());

	public static final String ROOT_PATH = "/";
	public static final String AUTH_PATH = "/auth";
	public static final String API_PATH = "/api";
	public static final String API_PATH_DOC = "/docs";
	public static final String ERROR_PATH = "/error";

	public static final String LOGIN_PAGE = AUTH_PATH + "/login";
	public static final String LOGIN_PROCESSING_URL = AUTH_PATH + "/sign_in";
	public static final String FAILURE_URL = AUTH_PATH + "/fail";
	public static final String USERNAME_PARAMETER = "username";
	public static final String PASSWORD_PARAMETER = "password";
	public static final String DEFAULT_SUCCESS_URL = "/home";
	public static final String LOGOUT_SUCCESS_URL = ROOT_PATH;
	public static final String SESSION_EXPIRED_URL = LOGIN_PAGE + "?expred";
	public static final String SESSION_INVALIDSESSION_URL = LOGIN_PAGE + "?invalid";
	public static final String LOGOUT_URL = AUTH_PATH + "/sign_out";
	public static final String REMEMBER_ME_KEY = "REMEBMER_ME_KEY";
	public static final String REMEMBER_ME_COOKE_NAME = "REMEMBER_ME_COOKE";
	
	//ignore check html
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/mainimg/**", "/scss/**", "/SpryAssets/**", "/vendor/**");
		web.ignoring().antMatchers(HttpMethod.GET, API_PATH + "/**");
		web.ignoring().antMatchers(HttpMethod.GET, API_PATH_DOC + "/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.anonymous()
			.and().authorizeRequests()
				.antMatchers(HttpMethod.GET, ROOT_PATH).permitAll()
				.antMatchers(HttpMethod.GET, "/docs/**").permitAll()
				.antMatchers(HttpMethod.GET, "/auth/**").permitAll()
				.antMatchers(HttpMethod.GET, "/login").permitAll()
				.anyRequest().hasAnyAuthority("ADMIN", "USER")
				//.anyRequest().permitAll()
			.and().sessionManagement()
				.maximumSessions(1)
				.expiredUrl(SESSION_EXPIRED_URL)
				.maxSessionsPreventsLogin(false)
			.and()
				.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
				.invalidSessionUrl(SESSION_INVALIDSESSION_URL)
			.and()
				.formLogin()
					.loginPage(LOGIN_PAGE) //로그인 폼 URL
					.loginProcessingUrl(LOGIN_PROCESSING_URL) //로그인 폼 Action Url 
					.failureUrl(FAILURE_URL) // 실패시 URl
					.usernameParameter(USERNAME_PARAMETER) //파라메터 설정
					.passwordParameter(PASSWORD_PARAMETER) //파라메터 설정
					.defaultSuccessUrl(DEFAULT_SUCCESS_URL) // 성공시 이동될 페이지
					//.failureHandler(authenticationFailureHandler())
					.successHandler(authenticationSuccessHandler()).permitAll()
				.and()
					.rememberMe().key(REMEMBER_ME_KEY)
					.rememberMeServices(tokenBasedRememberMeServices())
				.and()
					.logout()
					.deleteCookies(REMEMBER_ME_COOKE_NAME)
					.deleteCookies("JSESSIONID")
					.logoutUrl(LOGOUT_URL).invalidateHttpSession(true)
					.logoutSuccessUrl(LOGOUT_SUCCESS_URL)
				// .logoutSuccessHandler(logoutSuccessHandler()) //커스텀으로 로그아웃된거에 대한 처리를 해주면
				// 로그아웃성공URL로 가지 않으니 커스텀할떄 사용해여라
				.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_URL)).permitAll()
				.and()
					.authenticationProvider(authenticationProvider()) // configure(AuthenticationManagerBuilder auth) 오버라이딩해서
				.csrf().disable();

	}

	@Bean
	public AuthenticationProvider authenticationProvider() {
		return new AuthenticationProvider();
	}
	
	/* 디테일한 페이지 설정이 필요할때 사용
	@Bean
	public FilterSecurityInterceptor filterSecurityInterceptor() {
		FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
		filterSecurityInterceptor.setAuthenticationManager(authenticationManager);
		//filterSecurityInterceptor.setSecurityMetadataSource(filterInvocationSecurityMetadataSource());
		//filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
		return filterSecurityInterceptor;
	}
	*/
	/*
	@Bean
	public AffirmativeBased affirmativeBased() {
		List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
		accessDecisionVoters.add(roleVoter());
		AffirmativeBased affirmativeBased = new AffirmativeBased(accessDecisionVoters);
		return affirmativeBased;
	}
	

	@Bean
	public RoleHierarchyVoter roleVoter() {
		RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
		roleHierarchyVoter.setRolePrefix("ROLE_");
		return roleHierarchyVoter;
	}

	//RoleHierarchy 설정
	@Bean
	public RoleHierarchy roleHierarchy() {
		RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
		roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
		return roleHierarchy;
	}
	*/
	//시큐리트쪽 부분에서 사용자가 화면 페이지 호출하면 매번 호출되는 클래스 중요함
	/*
	@Bean
	public FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource() {
		return new FilterInvocationSecurityMetadataSource();
	}
	*/
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Bean
	public LoginUserDetailsService userDetailsService(){
		return new LoginUserDetailsService();
	}
	@Bean
	public RememberMeServices tokenBasedRememberMeServices() {
	  TokenBasedRememberMeServices tokenBasedRememberMeServices = new TokenBasedRememberMeServices(REMEMBER_ME_KEY, userDetailsService());
	  tokenBasedRememberMeServices.setAlwaysRemember(true);
	  tokenBasedRememberMeServices.setTokenValiditySeconds(60 * 60 * 24 * 31);
	  tokenBasedRememberMeServices.setCookieName(REMEMBER_ME_COOKE_NAME);
	  return tokenBasedRememberMeServices;
	}


	//login,out 정상처리 및 실패에 대한 Bean
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler();
    }
    /*
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        logger.debug("#### login Failurer handler #####");
        return new AuthenticationFailureHandler();
    }
    */
    //로그아웃 성공시 핸들링
    /*
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler(){
    	return new LogoutSuccessHandler();
    }
    */	
}

주석 처리 된 부분은 많다. 주석처리 한 부분까지 다 해보니 양이 너무 많아 그렇게까진 필요치 않아 축소했다. 

4. 인증과정 소스 생성(AuthenticationProvider)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import com.rest.gateway.Entity.LoginUserDetails;

public class AuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());

	@Autowired
	private LoginUserDetailsService userDetailsService;
	
	@Autowired
	private BCryptPasswordEncoder passwordEncoder;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		WebAuthenticationDetails detail = (WebAuthenticationDetails) authentication.getDetails();
		String remoteIP = detail.getRemoteAddress();
		String username = (String) authentication.getPrincipal();
		String password = (String) authentication.getCredentials();
		LoginUserDetails userDetails = userDetailsService.loadUserByUsername(username);
		logger.info("Login try ip :  -> " + remoteIP + " input id(" + username + ") info:" + userDetails);
		if (null == userDetails || userDetails.isAccountNonLocked() == false || userDetails.isAccountNonExpired() == false || userDetails.isEnabled() == false
				|| userDetails.isCredentialsNonExpired() == false) {
			throw new UsernameNotFoundException("로그인에 실패했습니다.");
		}
		
		if (!passwordEncoder.matches(password, userDetails.getPassword())) { // 실패
			throw new BadCredentialsException("로그인에 실패했습니다.");
		}

		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
	    token.setDetails(userDetails);
		return token;
	}

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

}

4-1. 설명 

암호화 하는 부분을 기존 클래스와 다르게 BCryptPasswordEncoder 를 사용했다. 기존과 좀 달라진 부분인거 같다. 

5. JPA DB연결 부분 생성

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.edentns.rest.gateway.Entity.LoginUserDetails;
import com.edentns.rest.gateway.repository.UserRepository;

public class LoginUserDetailsService implements UserDetailsService{
	
	@Autowired
	UserRepository userRepository;
	
	@Override
	public LoginUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		LoginUserDetails user = userRepository.findByUserId(userId);
		if (null == user) {
			throw new UsernameNotFoundException("로그인 유저가 없습니다.");
		}
		return user;
	}
	
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.edentns.rest.gateway.Entity.LoginUserDetails;


@Repository
public interface UserRepository extends JpaRepository<LoginUserDetails, Integer>{
	LoginUserDetails findByUserId(String userId);
}

5-1. 설명

기존 시큐리티 구조상 username 과 password 를 기본적으로 받는 구조인데, 테이블의 username 을 id 처럼 사용해야되는 줄 알았다가, 바꿀수 있는지 뒤늦게 알았다. 

6. 인증 성공시 핸들러 소스

import java.io.IOException;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

public class AuthenticationSuccessHandler implements org.springframework.security.web.authentication.AuthenticationSuccessHandler {
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());

	private RequestCache requestCache = null;
	private RedirectStrategy redirectStrategy = null;

	public AuthenticationSuccessHandler() {
		requestCache = new HttpSessionRequestCache();
		redirectStrategy = new DefaultRedirectStrategy();
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
		sendRedirectDefaultUrl(request, response);
	}

	private void sendRedirectSessionUrl(HttpServletRequest request, HttpServletResponse response) throws IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		String targetUrl = savedRequest.getRedirectUrl();
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}

	private void sendRedirectRefererUrl(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String targetUrl = request.getHeader("REFERER");
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}

	private void sendRedirectDefaultUrl(HttpServletRequest request, HttpServletResponse response) throws IOException {
		redirectStrategy.sendRedirect(request, response, SecurityConfiguration.DEFAULT_SUCCESS_URL);
	}

}

7. Jsp 폼 설정

<!-- 로그인 폼 -->
<form name="loginForm" id="loginForm" action="/auth/sign_in" method="post" class="user">
	<input name="username" placeholder="아이디를 입력하십시요.">
	<input name="password" placeholder="비밀번호를 입력하신시요.">
</form>

@RequestMapping("home")
public String home(Model model, Authentication authentication) {
	logger.info("authentication : {}", authentication);
	LoginUserDetails userDetails = (LoginUserDetails)authentication.getDetails();
	model.addAttribute("loginUserName", userDetails.getUsername());
	return "home";
}

<!-- 로그아웃 -->
<a href="/auth/sign_out" ${loginUserName}님 LOGOUT</a>

7-1. 설명

action 을 위의 config 소스에서 한것처럼 설정 username, password 도 위에 설정값에 있다. 

8. 부족한 점

폼을 이용한 로그인설정인데, 추가적으로 실패시 json 으로 떨궈주는 걸 추가해야한다. 

스프링 시큐리티 적용이 첨이라 어버버 하면서 적용했는데 아직 수정할 부분이 많다. 

참고한 해주세요.