https://dev-elop.tistory.com/43
위 내용 보고 스프링 시큐리티를 스프링 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 으로 떨궈주는 걸 추가해야한다.
스프링 시큐리티 적용이 첨이라 어버버 하면서 적용했는데 아직 수정할 부분이 많다.
참고한 해주세요.
'PROJECT > TOOL' 카테고리의 다른 글
Spring Boot 배포시 properties 못 찾을때 (0) | 2019.12.03 |
---|---|
Spring Boot 배포파일 생성시 JSP 추가 방법 (0) | 2019.12.03 |
이클립스 Line width 변경 (0) | 2019.11.08 |
Spring Boot index 페이지 설정 (0) | 2019.11.07 |
Spring Boot Jpa 적용 (0) | 2019.11.06 |