Dev/Spring.SpringBoot

[SpringBoot] [Inflearn - 무료] 스프링부트 시큐리티 & JWT 강의 (Section 0)

pu3vig 2023. 2. 8. 15:34
728x90
  • target:  스프링부트 시큐리티 & JWT 강의 1 ~ 5강 (섹션 0. 스프링 시큐리티 기본)

 


  • method: 

*시작 전 섹션0~섹션1 최신버전 업데이트 github 주소

(강사님 github 주소)
https://github.com/codingspecialist

(스프링 시큐리티 기본 V1)
https://github.com/codingspecialist/Sringboot-Security-Basic-V1

(구버전)
스프링부트 2.3.2 
자바 1.8
https://github.com/codingspecialist/Springboot-Security-OAuth2.0-V2

(신버전)
스프링부트 2.5.7
자바 11
https://github.com/codingspecialist/-Springboot-Security-OAuth2.0-V3

* Section 0. 스프링 시큐리티 기본

1. 스프링부트 시큐리티 1강 - 환경설정

  1.1) 실습을 위한 DB 계정 생성, 권한 설정 및 security DB 생성

 

  1.2) 실습을 위한 Springboot 프로젝트 생성

 

  1.3) application.yml에 기본적인 설정 - 서버포트 / 인코딩 / datasource / viewResolver / jpa

        ※ application.yml에서 jpa.hibernate.ddl-auto는 create로 하면 서버 기동 시, 스키마를 새로 생성

        ※ 최초 스키마 생성시에만 create로 생성

 

  1.4) Controller 생성 (src/main/java/com/cos/security1/controller/IndexController.java)

         ※ 이하 {baseDir} = src/main/java/com/cos/security1

 

  1.5) mustache(View템플릿) 기본 폴더는 (src/main/resources/)

 

  1.6) viewResolver는 application.yml에서 prefix / suffix가 설정이 가능 (pom.xml에 mustache를 추가했기 때문에, suffix는 기본이 .mustache)

 

  1.7) {baseDir}/config/WebMvcConfig.java에서 MustacheViewResolver를 통해 override 가능

// SecurityConfig.java

package com.cos.security1.config;

import org.springframework.boot.web.servlet.view.MustacheViewResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // TODO Auto-generated method stub
        // WebMvcConfigurer.super.configureViewResolvers(registry);
        MustacheViewResolver resolver = new MustacheViewResolver();
        
        resolver.setCharset("UTF-8");
        resolver.setContentType("text/html; charset=UTF-8");
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");

        registry.viewResolver(resolver);
    }
}

 

  1.8) 서버를 실행하고 localhost:8080 으로 접속하여 로그인 (아이디 : user // 패스워드 : 서버 실행 시, 아래와 같이 나옴)

 

스프링 시큐리티 로그인 화면
서버 실행 시 나옴

 

  1.9) localhost:8080/logout을 통해 로그아웃

 


2. 스프링부트 시큐리티 2강 - 시큐리티 설정

  2.1) IndexController에 "/user", "/admin", "/manager", "/loginForm", "/joinForm", "/join" 매핑 매소드 생성

// IndexController.java

package com.cos.security1.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.ResponseBody;

import com.cos.security1.model.User;
import com.cos.security1.repository.UserRepository;

@Controller // View를 리턴하겠다
public class IndexController {
    // localhost:8080/
    // localhost:8080
    @GetMapping({"", "/"})
    public String index() {
        // mustache default folder : src/main/resources/
        // ViewResolver Setting : templates (prefix), .mustache (suffix)
        // pom.xml에 dependency로 mustache를 추가한 경우, 생략 가능 (application.yml에 선언해주지 않아도 됨)
        return "index";
    }

    @GetMapping("/user")
    public @ResponseBody String user() {
        return "user";
    }

    @GetMapping("/admin")
    public @ResponseBody String admin() {
        return "admin";
    }

    @GetMapping("/manager")
    public @ResponseBody String manager() {
        return "manager";
    }

    // spring security에서 낚아챔!! - SecurityConfig 파일 생성 후, 작동 안함
    @GetMapping("/loginForm")
    public String loginForm() {
        return "loginForm";
    }

    @GetMapping("/joinForm")
    public String joinForm() {
        return "joinForm";
    }

    @PostMapping("/join")
    public String join(User user) {
        return "join";
    }
}

 

 

 

  2.2) WebSecurityConfigurerAdapter를 상속한 SecurityConfig.java 생성

※ user, manager, admin에 대해 권한을 적용하고, 이외의 URL은 접근허용(permitAll())을 적용

★스프링버전이 올라가면서 WebSecurityConfigurerAdapter가 deprecated되어 아래와 같이 변경 필요

// SecurityConfig.java

package com.cos.security1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	http.csrf().disable();
        http.authorizeHttpRequests()
            .requestMatchers("/user/**").authenticated()
            .requestMatchers("/manager/**").hasAnyRole("ROLE_ADMIN", "ROLE_MANAGER")
            .requestMatchers("/admin/**").hasRole("ROLE_ADMIN")
            .anyRequest().permitAll();
        return http.build();
    }
}

// 예전 버전 spring security
// 현재는 WebSecurityConfigurerAdapter가 deprecated되어 사용 불가
// @Configuration
// @EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
// public class SecurityConfig extends WebSecurityConfigurerAdapter {

//     @Override
//     protected void configure(HttpSecurity http) throws Exception {
//         http.csrf().disable();
//         http.authorizeRequests()
//             .antMatchers("/user/**").authenticated()
//             .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
//             .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
//             .anyRequest().permitAll();
//     }
// }

 

  2.3) SecurityFilterChain을 적용하면서 기존에 "/login" 접근 시, 스프링 시큐리티에서 가로채서 로그인 페이지로 redirect 되던 부분이 미동작하게 되고, 자체 제작한 login 페이지로 접근이 가능하게 됨

 

  2.4) 권한 적용으로 인하여 "/user", "/admin", "/manager"로 접근 시, 아래와 같은 403 에러 확인 가능

 

권한에 의한 접근 제약

 

  2.5) 아래와 같이 로그인하지 않은 경우, 로그인 페이지로 이동하도록 filterChain 추가

// SecurityConfig.java

package com.cos.security1.config;

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        ...
        (중략)
        ...
        
            .anyRequest().permitAll()
            /************** 아래부분 추가 **************/
            .and()
            .formLogin()
            .loginPage("/loginForm");
        return http.build();
    }
}

 


3. 스프링 시큐리티 3강 - 시큐리티 회원가입

  3.1) src/main/resources/templates/loginForm.html 파일 생성

<!-- loginForm.html -->

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
        <h1>로그인 페이지</h1>
        <hr/>
        <form>
            <input type="text" name="username" placeholder="Username"/><br/>
            <input type="password" name="password" placeholder="Password"/><br/>
            <button>로그인</button>
        </form>
        <a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
    </body>
</html>

 

  3.2) {baseDir}/model/User.java 생성 (Model을 통해 테이블 생성)

// User.java

package com.cos.security1.model;

import java.sql.Timestamp;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.hibernate.annotations.CreationTimestamp;

import lombok.Data;

@Entity
@Data
public class User {
    @Id // primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;
    private String email;
    private String role;    // ROLE_USER, ROLE_ADMIN
    @CreationTimestamp
    private Timestamp createDate;
}

모델을 통한 Table 생성

  3.3) {baseDir}/repository/UserRepository.java 생성

// UserRepository.java

package com.cos.security1.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.cos.security1.model.User;

// CRUD 함수를 JpaRepository가 들고 있음
// JpaRepository를 상속했으므로 @Repository가 없어도 IoC 가능
public interface UserRepository extends JpaRepository<User, Integer> {

}

 

  3.4) 회원가입 페이지 src/main/resources/templates/joinForm.html을 templates 디렉토리 안에 생성

<!-- joinForm.html -->

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>회원가입 페이지</title>
    </head>
    <body>
        <h1>회원가입 페이지</h1>
        <hr/>
        <form action="/join" method="POST">
            <input type="text" name="username" placeholder="Username"/><br/>
            <input type="password" name="password" placeholder="Password"/><br/>
            <input type="text" name="email" placeholder="Email"/><br/>
            <button>회원가입</button>
        </form>
    </body>
</html>

 

  3.5) 비밀번호를 암호화하여 저장하기 위한 {baseDir}/config/SecurityConfig.java에 BCryptPasswordEncoder 객체를 리턴하는 encodePwd() 메소드 생성

// SecurityConfig.java

package com.cos.security1.config;

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
public class SecurityConfig {
    
    /*************** 이 부분이 추가됨 ***************/
    // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }
    /*********************************************/

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        ...
        
    }
}

※ @Bean을 적용하였기에, 리턴되는 오브젝트를 IoC로 등록됨

 

  3.6) IndexController에 "/join" 메소드를 아래와 같이 변경

// IndexController.java

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    ...
    (중략)
    ...
    
    @GetMapping("/joinForm")
    public String joinForm() {
        return "joinForm";
    }

    @PostMapping("/join")
    public String join(User user) {
        System.out.println(user);

        user.setRole("ROLE_USER");

        /*
         * 패스워스 암호화
         * SecurityConfig.bCryptPasswordEncoder()에서 @Bean 등록 
         */
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword);

        userRepository.save(user);      // 회원가입
        
        return "redirect:/loginForm";
    }

※ "/join"메소드는 @GetMapping을 @PostMapping임


회원가입시 로그 (Hibernate: 부분이 출력됨)

 


테이블에 실제 들어간 데이터

 


4. 스프링부트 시큐리티 4강 - 시큐리티 로그

  4.1) {baseDir}/config/SecurityConfig.java에 로그인 기능 구현

// SecurityConfig.java

package com.cos.security1.config;

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
public class SecurityConfig {
    
    ...
    (중략)
    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        ...
        (중략)
        ...
        
            .and()
            .formLogin()
            .loginPage("/loginForm")
            /******************* 아래 추가 *******************/
            .loginProcessingUrl("/login")  // "/login" 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인 진행
            .defaultSuccessUrl("/");	// 일반적인 경우, 로그인 후 메인으로 이동하지만, 권한이 필요한 페이지 요청한 경우, 로그인 후, 해당 페이지로 자동 redirect
            /*************************************************/
        return http.build();
    }
}

※  Spring 시큐리티로 로그인을 진행하는 경우, 별도의 "/login" 관련 컨트롤러나 기능은 구현하지 않아도 가능


<!-- loginForm.html -->

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
        <h1>로그인 페이지</h1>
        <hr/>
        <form action="/login" method="POST">
            <input type="text" name="username" placeholder="Username"/><br/>
            <input type="password" name="password" placeholder="Password"/><br/>
            <button>로그인</button>
        </form>
        <a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
    </body>
</html>

※ <form>의 method는 반드시 "POST" 방식

 

  4.2) 로그인 계정 관련 사용자명/비밀번호/권한/ 기타 가능, 만료 여부 체크 기능 구현   {baseDir}/config/auth/PrincipalDetails.java 생성(auth 디렉토리 생성)

// PrincipalDetails.java

package com.cos.security1.config.auth;

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

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

import com.cos.security1.model.User;

/* 
 * 시큐리티가 "/login" 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
 * 로그인을 진행이 완료가 되면 session을 만들어줍니다. (Security ContextHolder)
 * Session : Security ContextHolder에 생성되고, Session 객체는 Authentication 타입
 * Authentication에는 User 정보를 보유
 * 
 * Security Session => Authentication => UserDetails(PrincipalDetails)
 */
public class PrincipalDetails implements UserDetails {

    private User user; // Composition

    public PrincipalDetails(User user) {
        this.user = user;
    }

    // 해당 User의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // TODO Auto-generated method stub
        Collection<GrantedAuthority> collect = new ArrayList<>();
        
        collect.add(new GrantedAuthority() {
            public String getAuthority() {
                return user.getRole();
            }
        });
        
        return collect;
    }

    @Override
    public String getPassword() {
        // TODO Auto-generated method stub
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        // TODO Auto-generated method stub
        return user.getUsername();
    }

    // 유효기간 만료 여부 (false가 만료)
    @Override
    public boolean isAccountNonExpired() {
        // TODO Auto-generated method stub
        return true;
    }

    // 계정 잠긴 여부 (false가 잠김)
    @Override
    public boolean isAccountNonLocked() {
        // TODO Auto-generated method stub
        return true;
    }

    // 계정 비밀번호 만료 여부 (false가 만료 X)
    @Override
    public boolean isCredentialsNonExpired() {
        // TODO Auto-generated method stub
        return true;
    }

    // 계정 활성화 여부 (false가 비활성)
    @Override
    public boolean isEnabled() {
        // TODO Auto-generated method stub

        /* 
         * 1년동안 회원이 로그인을 하지 않은 경우, 휴면 계정으로 처리하는 경우 (User.java 객체에 loginDate Field를 추가하여 최종 로그인 시간 기록)
         * 현재시간 - 로긴시간 => 1년 초과시 return false 하는 형식으로 기능 구현 필요
         */

        return true;
    }
}

※ isEnabled에 휴면계정 기능을 추가하고자 하는 경우, 주석 참조하여 직접 기능 구현 필요

 

  4.3) 로그인한 사용자의 정보를 객체에 담아서 Authentication 영역에 넘겨주는 서비스 기능 구현

  {baseDir}/config/auth/PrincipalDetailsService.java 생성

// PrincipalDetailsService.java

package com.cos.security1.config.auth;

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.stereotype.Service;

import com.cos.security1.model.User;
import com.cos.security1.repository.UserRepository;

/* 
 * 시큐리티 설정에서 loginProcessingUrl("/login");
 * "/login" 요청이 오면 자동으로 userDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행
 */
@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null) {
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

※ 메소드명 유지(loadUserByUsername)


★만약 loginForm 화면에서 username이라는 파라미터명을 사용하고 싶지 않은 경우, Securityconfig.java에 아래 추가

// Securityconfig.java

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
public class SecurityConfig {
    
    ...
    (중략)
    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        ...
        (중략)
        ...
        
            .and()
            .formLogin()
            .loginPage("/loginForm")
            
            /********************* loginForm에서 사용자ID가 username2인 경우 *********************/
            .usernameParameter("username2")  // 화면에서 로그인 id정보를 가져오는 파라미터 정보가 "username2"인 경우, 추가
            /************************************************************************************/
            
            .loginProcessingUrl("/login")  // "/login" 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인 진행
            .defaultSuccessUrl("/");    // 일반적인 경우, 로그인 후 메인으로 이동하지만, 권한이 필요한 페이지 요청한 경우, 로그인 후, 해당 페이지로 자동 redirect
        return http.build();
    }
}

 

  4.4) DB로부터 사용자 정보를 조회하는 메소드 생성

  {baseDir}/repository/UserRepository.java에 findByUsername 메소드 추가

// UserRepository.java

package com.cos.security1.repository;

...
(중략)
...

// CRUD 함수를 JpaRepository가 들고 있음
// JpaRepository를 상속했으므로 @Repository가 없어도 IoC 가능
public interface UserRepository extends JpaRepository<User, Integer> {

    // select * from user where username = ?
    public User findByUsername(String username);
}

※ 네이밍 규칙 (Spring Data JPA - Query Method 참조)

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference

 

  4.5) 로그인 시도 및 정상동작 확인

/loginForm에서 회원가입 링크 통해서 /joinForm 이동 및 가입


USER 테이블에 등록된 모습


로그인을 하는 경우 별도 URL 요청이 없었다면 홈으로 이동(defaultSuccessUrl)


/user를 요청해서 /loginForm을 간 경우, 로그인 성공 시, 바로 요청했던 페이지로 이동


localhost:8080/logout을 통해 session을 해지해주면 URL은 위와 같고 loginForm으로 이동 (더이상 /user로 접근 불가)

 


5. 스프링부트 시큐리티 5강 - 시큐리티 권한처리

  5.1) 특정 메소드에 권한 처리를 위해서 SecurityConfig.java에 @EnableGlobalMethodSecurity 추가

// SecurityConfig.java

package com.cos.security1.config;

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
@EnableGlobalMethodSecurity(securedEnabled=true)    // @Secured 활성화(단건 권한)
public class SecurityConfig {
	
    ...
    
}

※ EnableGlobalMethodSecurity(securedEnabled=true)를 적용함으로써 @Secured 어노테이션 사용 여부 활성화

 

  5.2) IndexController에 로그인한 사용자만 볼 수 있는 "/info" 메소드 생성 및 @Secured를 이용한 권한 적용

// IndexController.java

package com.cos.security1.controller;

...
(중략)
...

@Controller // View를 리턴하겠다
public class IndexController {
    
    ...
    (중략)
    ...
    
    @Secured("ROLE_ADMIN")	// 관리자만 접근 가능
    @GetMapping("/info")
    public @ResponseBody String info() {
        return "개인정보";
    }
}

 

  5.3) 특정 메소드에 다중 권한 적용을 위해 SecurityConfig.java에 @prePostEnabled 추가

// SecurityConfig.java

package com.cos.security1.config;

...
(중략)
...

@Configuration
@EnableWebSecurity      // Spring Security Filter가 Spring Filter chain에 등록됨
@EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)    // @Secured 활성화(단건 권한), @PreAuthorize/@PostAuthorize 활성화(다수 권한)
public class SecurityConfig {

	...
    
}

 

  5.4) IndexController에 "ROLE_MANAGER"와 "ROLE_ADMIN"만 접근 가능한 메소드 생성

// IndexController.java

package com.cos.security1.controller;

...
(중략)
...

@Controller // View를 리턴하겠다
public class IndexController {

    ...
    (중략)
    ...

    @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") // data실행 전 체크
    @GetMapping("/data")
    public @ResponseBody String data() {
        return "데이터정보";
    }
}

 


다음 Section 1. 스프링 시큐리티 OAuth2.0

https://pu3vig.tistory.com/110


  • warning: 

개발/프로그래밍 > 백엔드 > 스프링부트 시큐리티 & JWT 강의

[2023.02.08 기준] - 해당 강의 무료 시청 가능

추후 강의 유/무료가 바뀌거나 강의 내용이 업데이트 될 수 있음


  • source:

https://www.inflearn.com/

 

인프런 - 미래의 동료들과 함께 성장하는 곳 | IT 정보 플랫폼

프로그래밍, 인공지능, 데이터, 마케팅, 디자인, 엑셀 실무 등 입문부터 실전까지 업계 최고 선배들에게 배울 수 있는 곳. 우리는 성장 기회의 평등을 추구합니다.

www.inflearn.com

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference

 

Spring Data JPA - Reference Documentation

Example 119. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

728x90