[spring security] springboot에 가장 빠르게 적용해 보자[2]

 

이론편 보기

2020/06/28 - [spring boot] - [spring security] spring boot에 가장 빠르게 적용해 보자[1]

 

 

 

 

일단 DB에 접근할 수 있는 환경과 더불어 기본적인 mvc 패턴 vo,dao 정도로 구성이 다 짜여있다는 바탕 하에 진행해 보도록 하겠습니다.

저는 springboot + mybatis + mysql만으로 구현했습니다.

또한 필자는 다른 블로그에 비해 지식이 한없이 얕기 때문에 jpa를 통해 db에 접근한다던지던 지 enum을 사용해 사용자에게 권한을 준다 던지 이런 기술들은 제게 없습니다 어떻게 보면 가장 베이직한 기술만으로 적용을 했습니다.

 

필요한 파일: 

WebSecurityConfigurerAdapter 상속한 class 1개

UserDetails를 구현한 class 1개

UserDetailService를 구현한 class 1개

+@

 

진행하기에 앞서 인터넷에서 검색한 코드를 보다보면 어디서부터 온건지 뭔지 몰라 화가 나는 경우가 있었기 때문에

전체적인 구성을 올리겠습니다.

아직 완성되어있지 않은 토이 프로젝트라 감안하고 보시길 바랍니다.

 

각설하고 일단 springboot에 gradle로 라이브러리를 적용해 줍시다.

(maven으로 하시는 경우 maven repository으로 찾아봅시다 조금만 찾아보면 간단합니다)

 

build.gradle

compile 'org.springframework.boot:spring-boot-starter-security'

 

SecurityConfig.java (WebSecurityConfigurerAdapter)

package com.blackzapato.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.blackzapato.demo.service.SecurityService;

@Configuration //이 클래스를 통해 bean 등록이나 각종 설정을 하겠다는 표시
@EnableWebSecurity // Spring Security 설정할 클래스라고 정의합니다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	SecurityService homeService;

	@Bean //회원가입시 비번 암호화에 필요한 bean 등록
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean //실제 인증을 한 이후에 인증이 완료되면 Authentication객체를 반환을 위한 bean등록
	public DaoAuthenticationProvider authenticationProvider(SecurityService homeService) {
		DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
		authenticationProvider.setUserDetailsService(homeService);
		authenticationProvider.setPasswordEncoder(passwordEncoder());
		return authenticationProvider;
	}
	


	@Override
	public void configure(WebSecurity web) throws Exception {
		// static 디렉터리의 하위 파일 목록은 인증 무시 ( = 항상통과 )
		web.ignoring().antMatchers("/resource/**");
		
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
	
		http
		.authorizeRequests() 
			.antMatchers("/admin/**").hasAnyAuthority("ADMIN")// 관리자페이지를 위한 곳
			.antMatchers("/**").permitAll() 
			.anyRequest().authenticated()// 여기까지는 기본적으로 경로를 어떻게 사용할 것인가를 위한 곳이다
		.and()
			.csrf().ignoringAntMatchers("/admin/goods/ckUpload") //기본적으로 springSecurity에선 post로 controller로 정보를 보내줄떼 csrf라는 토큰이 필요하다 이것을 무시하기위한 경로
		.and()
			.formLogin()
			.loginPage("/member/signin")
			.failureUrl("/member/signin?error") /*로그인 실패시 url*/ 
			.defaultSuccessUrl("/", true) /*로그인 성공시 url*/
		.and()
			.logout()
			.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
			.deleteCookies("JSESSIONID")
			.invalidateHttpSession(true)
			.logoutSuccessUrl("/home")
		.and()
			.exceptionHandling()
			.accessDeniedPage("/access-denied");
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) {
		auth.authenticationProvider(authenticationProvider(homeService));
	}
}

 

MemberPrincipalVO.java (userDetails)

package com.blackzapato.demo.dto;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@SuppressWarnings("serial")
public class MemberPrincipalVO implements UserDetails{

private ArrayList<MemberVO> userVO;
	
	public MemberPrincipalVO(ArrayList<MemberVO> userAuthes) {
		this.userVO = userAuthes;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() { //유저가 갖고 있는 권한 목록

		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
		
		for(int x=0; x<userVO.size(); x++) {
			authorities.add(new SimpleGrantedAuthority(userVO.get(x).getRoleName()));
		}
		
		return authorities;
	}

	@Override
	public String getPassword() { //유저 비밀번호

		return userVO.get(0).getUserPass();
	}

	@Override
	public String getUsername() {// 유저 이름 혹은 아이디

		return userVO.get(0).getUserName();
	}

	@Override
	public boolean isAccountNonExpired() {// 유저 아이디가 만료 되었는지

		return true;
	}

	@Override
	public boolean isAccountNonLocked() { // 유저 아이디가 Lock 걸렸는지

		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() { //비밀번호가 만료 되었는지

		return true;
	}

	@Override
	public boolean isEnabled() { // 계정이 활성화 되었는지

		return true;
	}

}

 

SecurityService.java (UserDetailsService)

package com.blackzapato.demo.service;

import java.util.ArrayList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.blackzapato.demo.dao.MemberMapper;
import com.blackzapato.demo.dto.MemberPrincipalVO;
import com.blackzapato.demo.dto.MemberVO;

@Service
public class SecurityService implements UserDetailsService{
	
	@Autowired
	private MemberMapper homeMapper;

	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder;
	
	//DB에서 유저정보를 불러온다. Custom한 Userdetails 클래스를 리턴 해주면 된다.(실질적인 로그인코드)
	@Override
	public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
		System.out.println("id : "+id);
		ArrayList<MemberVO> userAuthes = homeMapper.findByUserId(id);
		System.out.println("userAuthes size : "+userAuthes.size());
		
		if(userAuthes.size() == 0) {
			throw new UsernameNotFoundException("User "+id+" Not Found!");
		}
		
		return new MemberPrincipalVO(userAuthes);
	}
	
    //회원가입도중 오류가 날수 있으므로 transactional을 사용합니다 모르면 구글갓에게 물어봅시다
	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
	public String InsertUser(MemberVO userVO) throws Exception {
		userVO.setUserPass(bCryptPasswordEncoder.encode(userVO.getUserPass()));
		int flag = homeMapper.signup(userVO);	
		System.out.println("flag num : "+flag);
		
		if (flag > 0) {
			userVO.setRoleName("USER");
			int roleNo = homeMapper.findRoleNo(userVO.getRoleName());
			System.out.println("roleNo : "+roleNo);
			String id = userVO.getUserId();
			homeMapper.userRoleSave(id, roleNo);

			return "success";
		}	 	
		return "fail";
	}


}

 

HomeController.java 

@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	@Autowired
	MemberMapper mm;
	
	@Autowired
	SecurityService hs;

    @GetMapping("/") //로그인 성공시 위에서 설정해준대로 맵핑이 되기때문에 이곳만 보여드리겠습니다.
    //다른 부분은 그냥 하시던 그대로 하시면 됩니다
    public String loadExceptionPage(HttpServletRequest req, RedirectAttributes rtt){
    	logger.info("signin after contoller");
    	Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 
		MemberPrincipalVO userPrincipalVO = (MemberPrincipalVO) auth.getPrincipal();
		//위의 두줄로 로그인 한 계정이 어떤 권한을 가지고 있는지 이름은 무었인지를 가져옵니다
		
		HttpSession session = req.getSession();
		
		System.out.println("username : "+userPrincipalVO.getUsername());
		System.out.println("authorities : "+userPrincipalVO.getAuthorities());
		 
	    session.setAttribute("member", userPrincipalVO.getUsername());
		session.setAttribute("principal", userPrincipalVO.getAuthorities());
		
    return "home"; 
    }
    }

이 정도면 실행되리라 생각합니다.

 

 

 

 

끝으로 적용은 했지만 더 자세히 알고 싶다 하시는 분들을 위해..

https://hyunsangwon93.tistory.com/24

 

스프링 부트 시큐리티(spring boot security) 시작 [1]

서론 Spring Security는 Spring으로 웹을 개발한다면 필수적인 프레임워크이다. 왜일까? 기본적인 세션관리 HTTP 보안 및 어플리케이션 보안을 담당해준다. 초기 설정은 생각보다 복잡하다. 그리고 개발자마다 시..

hyunsangwon93.tistory.com

https://victorydntmd.tistory.com/328

 

[SpringBoot] Spring Security를 이용한 회원가입/로그인/로그아웃

이번 글에서는 Spring Security를 이용하여 회원가입 및 로그인을 구현해보도록 하겠습니다. 전체 코드는 깃헙을 참고하시길 바랍니다. 개발환경 IntelliJ 2019.02 Java 11 SpringBoot 2.1.9 Gradle 5.6 라이브러..

victorydntmd.tistory.com

https://coding-start.tistory.com/153

 

Spring boot - Spring Security(스프링 시큐리티) 란? 완전 해결!

오늘 포스팅할 내용은 Spring Security이다. 사실 필자는 머리가 나빠서 그런지 모르겠지만, 아무리 구글링을 통해 스프링 시큐리티를 검색해도 이렇다할 명쾌한 해답을 얻지 못했다. 대부분 이론적인 설명들은..

coding-start.tistory.com

https://myserena.tistory.com/158

 

리다이렉션 횟수가 너무 많습니다.

Path설계를 잘못하면 위와 같은 페이지를 만날 수 있다. Security에서 Path의 권한 설정은 위에서 부터 차례대로 적용된다. 아래의 코드를 보면 anyRequest()는 “USER"권한을 가지고 접근가능하지만 로그인을 하..

myserena.tistory.com

https://to-dy.tistory.com/75

 

Spring Security - 경로에 따른 접근 권한 설정

스프링 시큐리티에서는 권한에 따라 접근 가능한 경로를 제한할 수 있다. 그럼 권한에 따라 다른 링크를 보여준다면, 접근 가능한 경로를 제한할 수 있는 것 아닌가? 라는 생각을 할 수도 있다. 맞다. 눈에 보이는..

to-dy.tistory.com