SpringBoot 환경에서 Security를 이용한 접근권한 관리하기
Spring Security는 스프링 기반 어플리케이션의 보안과 인증을 담당하는 프레임워크로, 사용자 인증 및 보안 처리를 간단하게 구현할 수 있다.
이 글은 SpringBoot 2.7.8 환경에서 접근권한을 설정하였으며, Spring MVC 환경의 설정은 이전글 보기를 참고한다.
1. 개발환경
- Eclipse 2021-06 (4.20.0) + Spring Tools 4
- Java 1.11
- SpringBoot 2.7.8
- Spring 5.3.25
- Tomcat 9.0
- MySQL
2. Security dependency 추가
@ pom.xml
<properties>
<version>2.7.8</version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 스프링부트 프로젝트 Dependencies
3. application.properties 설정
@ application.properties
# spring security 설정
spring.security.user.name = admin
spring.security.user.password = 1122
4. Security Config 클래스 작성
@ HttpSecurityConfig.java
@Configuration
@EnableWebSecurity
public class HttpSecurityConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/static/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/sign/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/shop/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/mypage/**").authenticated() // 로그인한 모든 사용자 접근 허용
.antMatchers("/product/**").hasAnyRole("AD") // 로그인한 관리자만 접근 허용
.antMatchers("/member/**").hasRole("AD") // 로그인한 관리자만 접근 허용
.anyRequest().anonymous() // 인증 불필요
.and()
.formLogin();
return http.build();
}
}
- security 관련 dependency를 추가하면, 별도의 설정을 하지 않아도 인증기능이 활성화 되어 login 페이지로 이동
- application.properties에 설정한 user 정보를 입력하면 로그인 성공
🠗 로그인 페이지 및 로그인 성공/실패에 대한 인증 커스텀
5. Security Config 클래스 수정
@ HttpSecurityConfig.java
@Configuration
@EnableWebSecurity
public class HttpSecurityConfig {
/*
로그인 설정 : start
springboot에서 기본 제공하는 페이지를 사용할 경우 설정하지 않아도 됨.
*/
private final String SIGN_FAILURE_URL = "/sign/login?error=fail";
@Bean(name="authManager")
public AuthenticationManager authManager() {
return new ProviderManager(authProvider());
}
@Bean(name="failHandler")
public SignAuthenticationFailHandler failHandler() {
return new SignAuthenticationFailHandler(SIGN_FAILURE_URL);
}
@Bean(name="successHandler")
public SignAuthenticationSuccessHandler successHandler() {
return new SignAuthenticationSuccessHandler();
}
@Bean(name ="authProvider")
public SignAuthenticationProvider authProvider() {
return new SignAuthenticationProvider(authService());
}
@Bean(name ="authService")
public SignAuthenticationService authService() {
return new SignAuthenticationService();
}
public BCryptPasswordEncoder passwdEncoder() {
return new BCryptPasswordEncoder();
}
/*
로그인 설정 : end
*/
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/static/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/sign/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/shop/**").permitAll() // 로그인 없이 모든 접근 허용
.antMatchers("/mypage/**").authenticated() // 로그인한 모든 사용자 접근 허용
.antMatchers("/product/**").hasAnyRole("AD") // 로그인한 관리자만 접근 허용
.antMatchers("/member/**").hasRole("AD") // 로그인한 관리자만 접근 허용
.anyRequest().anonymous() // 인증 불필요
.and()
.formLogin()
/*
로그인 설정 : start
여기서부터는 로그인 페이지를 직접 설정한 경우로
springboot에서 기본 제공하는 페이지를 사용할 경우 설정하지 않아도 됨.
*/
.loginPage("/sign/login")
.usernameParameter("id")
.passwordParameter("pwd")
.successHandler(successHandler())
.failureHandler(failHandler());
/*
로그인 설정 : end
*/
return http.build();
}
}
6. 인증관련 UserSignDetails 클래스 생성
@ UserSignDetails.java
/*
Spring Security UserDetails 구현체
*/
public class UserSignDetails implements UserDetails {
private MemberVo member;
private Collection<? extends GrantedAuthority> authorities;
private boolean enabled;
private boolean credentialsNonExpired;
private boolean accountNonExpired;
public UserSignDetails(MemberVo member) {
this.member = member;
// 초기데이타는 true -> db값에 따라 차후 변경
this.enabled = true;
this.credentialsNonExpired = true;
this.accountNonExpired = true;
}
public MemberVo getMember() {
return this.member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return member.getPwd();
}
@Override
public String getUsername() {
return member.getId();
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
@Override
/**
* 계정만료여부
*/
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
/**
* 계정잠금여부
*/
public boolean isAccountNonLocked() {
return true;
}
@Override
/**
* 계정비밀번호만료여부
*/
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
/**
* 계정활성화여부
*/
public boolean isEnabled() {
return this.enabled;
}
7. 인증관련 UserAuthenticationService 클래스 생성
@ UserAuthenticationService.java
/*
Spring Security UserDetailsService 구현체
*/
@Service("com.onda2me.app.security")
public class SignAuthenticationService implements UserDetailsService {
@Autowired
private MemberMapper memberMapper;
public SignAuthenticationService() {
}
public SignAuthenticationService(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public UserSignDetails loadUserByUsername(String id) throws UsernameNotFoundException {
MemberEntity member = memberMapper.select(id);
if(member == null) {
throw new UsernameNotFoundException(id + " UsernameNotFound ");
}
return UserSignDetails(EntityMapperUtil.map(member, MemberVo.class));
}
}
8. 인증관련 SignAuthenticationProvider 클래스 생성
@ SignAuthenticationProvider.java
/*
Spring Security AuthenticationProvider 구현체
사용자의 아이디와 비밀번호로 인증 수행
*/
@Component
public class SignAuthenticationProvider implements AuthenticationProvider {
private Logger logger = LoggerFactory.getLogger(SignAuthenticationProvider.class);
@Autowired
private SignAuthenticationService authService;
public SignAuthenticationProvider(SignAuthenticationService authService) {
this.authService = authService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
BCryptPasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
String userId = (String)authToken.getName();
String userPwd = (String)authToken.getCredentials();
String loginChannel = null;
// STEP1. 사용자정보 조회
UserSignDetails userDetails = authService.loadUserByUsername(userId);
// STEP2. 비밀번호 체크
if(!pwdEncoder.matches(userPwd, userDetails.getPassword())) {
logger.error("authenticate.user.pwd --------------- BadCredentialsException");
throw new BadCredentialsException(userId + "Invalid password");
}
// STEP3. ROLE 설정
List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();
roles.add(new SimpleGrantedAuthority("ROLE_" + String.valueOf(userDetails.getMember().getRoleCode())));
return new UsernamePasswordAuthenticationToken(userDetails, userPwd, roles);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
9. 인증관련 SignAuthenticationSuccessHandler 클래스 생성
@ SignAuthenticationSuccessHandler.java
/*
Spring Security 로그인 성공 구현체
*/
@Component
public class SignAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SignAuthenticationService authService;
public SignAuthenticationSuccessHandler(SignAuthenticationService authService) {
this.authService = authService;
}
public SignAuthenticationSuccessHandler() {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
// 로그인 성공 Process
authSuccProcess(request.getSession(), response, request, authentication);
super.onAuthenticationSuccess(request, response, authentication);
}
/**
* 로그인 성공시 세션 정보 세팅
*
* @param session
* @param response
* @param authentication
* @throws IOException
*/
private void authSuccProcess(HttpSession session, HttpServletResponse response, HttpServletRequest request, Authentication authentication) throws IOException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
LoginVo loginVo = null;
//----------------------------------------------------------------------
// STEP1. Session & Cookie 저장
//----------------------------------------------------------------------
loginVo = new LoginVo(((UserSignDetails)authToken.getPrincipal()).getMember());
loginVo.setSessionId(request.getRequestedSessionId());
SessionUtil.addLoginSession(session, loginVo);
}
}
10. 인증관련 SignAuthenticationFailHandler 클래스 생성
@ SignAuthenticationFailHandler.java
/*
Spring Security 로그인 실패 구현체
*/
@Component
public class SignAuthenticationFailHandler implements AuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private String defaultFailureUrl;
public SignAuthenticationFailHandler() {
}
public SignAuthenticationFailHandler(String defaultFailureUrl) {
setDefaultFailureUrl(defaultFailureUrl);
}
public String getDefaultFailureUrl() {
return defaultFailureUrl;
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
this.defaultFailureUrl = defaultFailureUrl;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String errorMessage;
if (exception instanceof UsernameNotFoundException) {
errorMessage = "아이디가 존재하지 않습니다. ";
}
else if (exception instanceof BadCredentialsException) {
errorMessage = "아이디 또는 비밀번호가 일치하지 않습니다.";
}
else if (exception instanceof CredentialsExpiredException) {
errorMessage = "아이디 사용기한이 만료되었습니다.";
}
else if (exception instanceof DisabledException) {
errorMessage = "아이디 사용이 중지되었습니다.";
}
else if (exception instanceof AuthenticationCredentialsNotFoundException) {
errorMessage = "인증 요청이 거부되었습니다. ";
}
// ETC Exception
else {
errorMessage = "알 수 없는 이유로 로그인에 실패하였습니다";
}
request.setAttribute("errorMessage", errorMessage);
request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
}
}
11. 인증관련 로그인 컨트롤러 생성
@ SignController.java
@Controller
public class SignController {
private static final Logger logger = LoggerFactory.getLogger(SignController.class);
@RequestMapping(value = "/sign/login")
public String login(Authentication auth, @RequestParam HashMap<String, Object> paramMap) {
logger.debug("\n\n--------------------------------------------------");
logger.debug("SignController.login");
logger.debug("auth: " + auth);
logger.debug("paramMap: " + paramMap);
logger.debug("--------------------------------------------------\n\n");
return "/html/sign/login";
}
}
12. 인증관련 로그인 페이지 생성
@ /sign/login.html
<form action="/sign/login" method="post">
<input type="hidden" th:name="_csrf" th:value="${_csrf.token}" />
<div class="shadow p-4 m-4 bg-white">
<div class="login-container ">
<div class="row main-head ">
<div class="h2 fw-bolder w3-center ">
Login
</div>
</div>
<label for="uname"><b>User Id</b></label>
<input type="text" placeholder="Enter Username" name="id" required>
<label for="psw"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="pwd" required>
<div th:if="${param.error}" class="alert alert-warning my-3 py-3">
<strong>error!!</strong> : <span th:text=${errorMessage}></span>
</div>
<div class="row">
<div class="col-6"><button type="submit">Login</button></div>
<div class="col-6"><button type="button" class="cancelbtn" onclick="goHref('/')">Cancel</button></div>
</div>
</div>
</div>
</form>
13. 웹에서 확인
@ 로그인 페이지
@ 로그인 실패 페이지
댓글남기기