Spring

[SpringBoot] Security

shb 2022. 5. 16. 14:54

Authentication (인증)   /  Authorization (인가)
인증(Authentication) : 자신을 증명
- 내가 (스스로) 자신을 증명할 만한 자료를 제시 하는 것.
- 시스템 접근 시, 등록된 사용자인지 여부를 확인하는 것
- 로그인

인가(Authorization) : 권한 부여
- 접근 후, 인증된 사용자에게 권한을 부여하는 것
- 남에 의해서 ‘자격’ 이 부여된것.
- 권한에따라 사용 가능한 기능이 제한됨
- 사용자 등급(ex: 일반/VIP/관리자)

 

서버로 들어오는 request를 낚아챌 수단이 필요하다.
‘request’ 를 처리하기 전에 보안체크부터 해야 한다.  검문소 역할을 해야 하는데
스프링 시큐리티는 서블릿 프로그래밍의 filter 객체를 사용하여 이를 구현한다.

 

filter 
서블릿 2.3 부터 도입
'HTTP 요청과 응답을 변경할 수 있는 재사용가능한 코드'이다. 

필터는 ‘객체의 형태’로 존재하며 클라이언트로부터 오는 요청(request)과 최종 자원(서블릿/JSP/기타 문서) 사이에 위치하여 클라이언트의 요청 정보를 알맞게 변경할 수 있으며, 또한 필터는 최종 자원과 클라이언트로 가는 응답(response) 사이에 위치하여 최종 자원의 요청 결과를 알맞게 변경할 수 있다.

 

Security Filter Chain
스프링 시큐리티도 서블릿 필터를 사용한다.
Security와 관련한 서블릿 필터도 실제로는 연결된 여러 필터들로 구성 되어 있다. 이러한 모습때문에 Chain(체인)이라 표현한다.

 

* IndexController 작성

package com.lec.spring.controller;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.lec.spring.domain.UserDTO;
import com.lec.spring.service.UserService;

@Controller
public class IndexController {
	
	@Autowired
	UserService userService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	

	@RequestMapping({"", "/"})
	@ResponseBody
	public String sayHello() {
		return "<h2>/ : Hello</h2>";
	}
	
	// Spring Security(이하 '시큐리티') 가 적용되면
	// /login 등의 url 로의 request 를  시큐리티가 모두 낚아 챕니다.
	// ※ 나중에 SecurityConfig 가 설정되면 낚아 채지 않게 된다.
	@GetMapping("/login")
//	@ResponseBody
	public String login() {
	    System.out.println("GET: /login");
//	    return "<h2> /login : login 페이지</h2>";
	    return "loginForm";
	}
	
	@PostMapping("/login")
	public String loginFail() {
		System.out.println("POST: /login");
		return "loginForm";
	}
	

	//  현재 로그인한 정보 Authentication 보기
	@RequestMapping("/auth")
	@ResponseBody
	public Authentication auth(HttpSession session) {
		return SecurityContextHolder.getContext().getAuthentication();
		//시큐리티가 /loginOk 주소요청이 오면 낚아채서 로그인을 진행시킨다.
		//로그인 진행이 완료되면 '시큐리티 session' 에 넣어주게 된다. 
		//우리가 익히 알고 있는 같은 session 공간이긴 한데..
		//시큐리티가 자신이 사용하기 위한 공간을 가집니다. 
		//=> Security ContextHolder 라는 키값에다가 session 정보를 저장합니다.
		//여기에 들어갈수 있는 객체는 Authentication 객체이어야 한다.
		//Authentication 안에 User 정보가 있어야 됨. 
		//User 정보 객체는 ==> UserDetails 타입 객체이어야 한다.

	}
	
    @GetMapping("/join")
    public String join() {
        return "joinForm";
    }
	
    @PostMapping("/joinOk")
    public String joinOk(UserDTO user) {
        System.out.println("/joinOk: " + user);

        // password 는 암호화 하여 저장
        String rawPassword = user.getPw();
        String encPassword = passwordEncoder.encode(rawPassword);
        user.setPw(encPassword);
        
        userService.addMember(user);
        
        return "redirect:/login";
    }
    
    @RequestMapping("/accessError")
    public void accessError() {}

	
}

* SampleController.java 작성

package com.lec.spring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/sample/*")
public class SampleController {
	
	@GetMapping("/all/**")
	@ResponseBody
	public String doAll() {
		return "<h1>/all/** : 누구나 접근 가능</h1>";
	}
	
    @GetMapping("/user/**")
    @ResponseBody
    public String doUser() {
        return "<h1>/user/** : 로그인한 사람이면 접근 가능</h1>";
    }

    @GetMapping("/member/**")
    @ResponseBody
    public String doManager() {
        return "<h1>/member/** : 로그인한 사람중 'ROLE_MEMBER' 나 'ROLE_ADMIN' 권한만 접근 가능</h1>";
    }

    @GetMapping("/admin/**")
    @ResponseBody
    public String doAdmin() {
        return "<h1>/admin/** : 로그인 한 사람중 'ROLE_ADMIN' 권한가진 사람만 접근 가능</h1>";
    }

	
} // end controller

* Spring Security Starter 추가

 

일단 Spring Security (이하 ‘시큐리티’)가 적용되면,  Security Filter Chain 이 가동된다.
(기본적으로) 모~ 든 페이지가 ‘인증’ 이 필요한 페이지로 동작(redirect)하게 됩니다. 
즉, 어떠한 url 로의 request 접근도 ‘시큐리티’ 가 낚아챕니다. (intercept)

 

* SecurityConfig 생성
{base-package}/config/
SecurityConfig.java 생성
스프링 시큐리티에 대한 설정들이 이 안에서 메소드 체이닝으로 설정된다.

 

// 스프링 시큐리티 설정
@Configuration
@EnableWebSecurity  // Web Security 를 활성화 해준다.
					// 아래 스프링 시큐리티 필터가 스프링 필터 체인에 등록이 된다.
			// ↓ 등록될 필터 객체
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
    // PasswordEncoder 를 bean 으로 IoC 에 등록
    // IoC 에 등록된다, IoC 내에선 어디서든 가져다가 사용할수 있다.
	@Bean
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();    // CSRF 비활성화  (Cross Site Request Forgery)

‘권한’ 기능을 부여해서
/sample/user/** 는 ← 로그인한 사람만
/sample/member/** 는 ← 회원
/sample/admin/** 은 ← 관리자만 
접근할수 있게 설정

 

* 접근 권한 세팅 진행
스프링 시큐리티에서 사용하는 권한명은 ‘ROLE_’ 로 시작하는 접두어를 갖고 동작하도록 기본 설정되어 있습니다.

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();    // CSRF 비활성화  (Cross Site Request Forgery)
		
		http  // HttpSecurity 객체
			
			/**********************************************
			 * ① request URL 에 대한 접근 권한 세팅
		     * ExpressionInterceptUrlRegistry 객체 리턴.  이하 이 객체의 메소드체이닝
		     **********************************************/
			.authorizeRequests()
		
			// URL 과 접근권한 세팅(들)
	        // ↓ /sample/user/** 주소로 들어오는 요청은 '인증'만 필요.
			.antMatchers("/sample/user/**").authenticated()
	        // ↓ /sample/member/** 주소로 들어오는 요청은 '인증' 뿐 아니라 ROLE_MEMBER 나 ROLE_ADMIN 권한을 갖고 있어야 한다 ('인가')
	        .antMatchers("/sample/member/**").access("hasRole('ROLE_MEMBER') or hasRole('ROLE_ADMIN')")	                
	        // ↓ /sample/admin/**  주소로 들어오는 요청은 '인증' 뿐 아니라 ROLE_ADMIN 권한을 갖고 있어야 한다 ('인가')
			.antMatchers("/sample/admin/**").access("hasRole('ROLE_ADMIN')")
			// ↓ 그 밖의 다른 요청은 모두 permit! (위 주소들만 아니면 누구나 접근 가능!)
			.anyRequest().permitAll()

로그인 설정, 커스텀 로그인 페이지

 

.formLogin()
SecurityConfig.java

			.and()   // 다시, HttpSecurity 리턴,  다른 세팅 전환시 호출
			
			/******************************************** 
			 * ② 폼 로그인 설정
			 * FormLoginConfigurer<HttpSecurity> 리턴.  이하 이 객체의 메소드체이닝
			 ********************************************/
			.formLogin()  // form 기반 인증 페이지 활성화.  만약 .loginPage(url) 가 세팅되어 있지 않으면 '디폴트 로그인' form 페이지가 활성화 된다

.loginPage(url)     로그인 form

			.loginPage("/login")  // 로그인 필요한 상황 발생시 매개변수의 url (로그인 폼) 로 redirect 발생

여기까지 동작확인
접근권한 오류시 /login으로 이동하게 되고 아까와 달리 달리 우리가 제공한 로그인 페이지가 보인다.
이제 view 로 form을 보여주기.

 

* 로그인시 입력한 username, password 처리하기

SecurityConfig.java

			// 로그인 처리
			.loginProcessingUrl("/loginOk")  // "/loginOk" url 로 POST request 가 들어오면 시큐리티가 낚아채서 처리, 대신 로그인을 진행해준다.
										// 이와 같이 하면 Controller 에서 /longinOk 를 만들지 않아도 된다!
			.defaultSuccessUrl("/")   // '직접 /login' → /loginOk 에서 성공하면 "/" 로 이동시키기
		      			// 만약 다른 특정페이지에 진입하려다 로그인 하여 성공하면 해당 페이지로 이동 (너무 편리!)

IndexController.java

	@GetMapping("/login")
//	@ResponseBody
	public String login() {
	    System.out.println("GET: /login");
//	    return "<h2> /login : login 페이지</h2>";
	    return "loginForm";
	}

loginForm.jsp 작성
{view_root}/loginForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
<style>
.error {color: red}
</style>
</head>
<body>
<h1>로그인 페이지</h1>
<hr>
<!-- loginProcessingUrl() 로 세팅한 url, 반드시 POST! -->
<form action="/loginOk" method="POST">  <!-- 시큐리티로 /loginOk 을 낚아챈다.  무조건 POST -->
    <!-- ↓ 아이디/패스워드 의 name 은 'username' 과 'password' 로 하자 (시큐리티의 디폴트) -->
    <input type="text" name="username" placeholder="아이디 입력" value="${username }"/><br>
    <input type="password" name="password" placeholder="패스워드 입력"/><br>
    <br>
    <span class="error">${errorMessage }</span><br>
    <input type="submit" value="로그인"/>
</form>
<a href="/join">회원가입을 아직도 안하셨나요?</a>
</body>
</html>

동작확인
/login (혹은 다른 인증이 필요한 url 로 접근시)
우리가 만든 커스텀 로그인 페이지가 뜬다.

 

username, password  입력하면 로그인 동작 수행

 

디폴트 username, password 변경하기
매번 서버가동할때 뜨는 디폴트 패스워드 복붙하기 싫다면
application.properties에서 설정

 

권한 맛보기
현재의 디폴트 계정으로 로그인 하면 /sample/user/**   만 가능하고
/sample/member/**  ,   /sample/admin/** 는 안된다.

 

application.properties
# 디폴트 권한 설정, 앞의 ROLE_  은 빼고 지정
spring.security.user.roles=MEMBER

서버 재시작 하면 일단 /logout 한뒤 다시 로그인 
그리고 /sample/member/** 페이지로 들어가보면 -> 접근됨
반면 /sample/admin/** 페이지로 들어가보면 -> 접근 불가

 

# 디폴트 권한 설정, 앞의 ROLE_  은 빼고 지정
spring.security.user.roles=ADMIN 

-> ROLE_ADMIN 권한 지정

 

서버 재시작 하면 일단 /logout 한뒤 다시 로그인 
그리고 /sample/member/** 페이지로 들어가보면 -> 접근됨
반면 /sample/admin/** 페이지로 들어가보면 -> 접근됨

 

# 디폴트 권한 설정, 앞의 ROLE_  은 빼고 지정
spring.security.user.roles=ADMIN,MEMBER 

-> ROLE_MEMBER 와 ROLE_ADMIN 권한 지정

    하나의 계정에 복수개 권한 지정 가능

 

/sample/member/** 페이지로 들어가보면 -> 접근됨
반면 /sample/admin/** 페이지로 들어가보면 -> 접근됨

 

현재 로그인 한 정보는?
현재 인증(로그인) 한 정보는 Authentication 객체로 받아올수 있다.
IndexController.java

	//  현재 로그인한 정보 Authentication 보기
	@RequestMapping("/auth")
	@ResponseBody
	public Authentication auth(HttpSession session) {
		return SecurityContextHolder.getContext().getAuthentication();
		//시큐리티가 /loginOk 주소요청이 오면 낚아채서 로그인을 진행시킨다.
		//로그인 진행이 완료되면 '시큐리티 session' 에 넣어주게 된다. 
		//우리가 익히 알고 있는 같은 session 공간이긴 한데..
		//시큐리티가 자신이 사용하기 위한 공간을 가집니다. 
		//=> Security ContextHolder 라는 키값에다가 session 정보를 저장합니다.
		//여기에 들어갈수 있는 객체는 Authentication 객체이어야 한다.
		//Authentication 안에 User 정보가 있어야 됨. 
		//User 정보 객체는 ==> UserDetails 타입 객체이어야 한다.

	}

sesstion attribute에 SPRING_SECURITY_CONTEXT 라는 이름으로 현재 로그인 정보가 담겨 있다
그리고 그 정보는 Authentication 객체다.

'Spring' 카테고리의 다른 글

[SpringBoot] Security - 회원가입 DB 인증  (0) 2022.05.16
[SpringBoot] MyBatis  (0) 2022.05.13
[Spring Boot] Validation  (0) 2022.05.12
[SpringBoot] Request Parameter  (0) 2022.05.12
[SpringBoot] RequestMapping  (0) 2022.05.12