BE/Spring

[Spring Security] 계정 생성, 로그인 및 권한 처리

멍목 2022. 5. 30. 22:02
반응형

안녕하세요.

 

Spring Security에 대해서 공부하고 알게되는 내용에 대해서 정리해보려고 합니다.

스스로 간단하게 정리를 하기에 주석에다가 정리를 하였고, 공부하였으니 보기 불편하실 수도 있습니다.

 

이 포스팅은 아래의 강의를 참고하였으니 여기에서 공부하시는 것을 추천드립니다. 

https://inf.run/tcLk

 

[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의

스프링부트 시큐리티에 대한 개념이 잡힙니다., - 강의 소개 | 인프런...

www.inflearn.com

 


들어가기 앞서, 프로젝트 개발 환경(필자 기준)

  • 인텔리제이
  • H2 DB
  • Spring Boot 2.7.0
  • maven project

추가한 라이브러리

  • spring-data-jpa : jpa를 편하게 사용하기 위해
  • mustache : 정적 템플릿 사용을 도와줌
  • spring security : 이번 학습 목표. Spring security
  • spring-boot-devtools : Spring 에서 개발을 편하게 해주는 라이브러리
  • h2database : H2 DB와 연결 할 수 있게 도와주는 라이브러리
  • spring-boot-web : Web으로 개발 할 수 있게 도와주는 라이브러리
  • lombok : getter, setter, toString 등 엔티티를 편하게 작성할 수 있게 도와주는 라이브러리

pom.xml 일부 (참고)

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mustache</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-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

 

 

스프링 시큐리티 설정

WebSecurityConfigurerAdapter를 상속받아서 설정 파일을 생성하기

@Configuration			// 설정과 관련된 클래스파일임을 알리는 어노테이션
@EnableWebSecurity      // spring Security filter가 Spring filter chain 에 등록
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	...
}

 

 

계정 생성, 로그인, 권한 처리 방법

1. SecurityConfig 파일(WebSecurityConfigurerAdapter을 상속받은 설정 파일)

package com.cos.security1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity      // spring security filter가 Spring filter chain 에 등록

// securedEnabled: @Secured 어노테이션 활성화. @Secured? 특정 URL에 대해서만 간단하게 권한 처리를 할 수 있는 어노테이션
// prePostEnabled: @PreAuthorize, @PostAuthorize 어노테이션 활성화.
// @PreAuthorize: 해당 메소드 진입 전 처리. @PostAuthorize: 해당 메소드 진입 후 처리
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * password 암호화 메소드. 추후 계정 생성 시, 사용함
     * @return
     */
    @Bean
    public BCryptPasswordEncoder encodePw(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{       // Spring Security 설정
        http.csrf().disable();      // csrf 비활성화

        // URL에 따른 접근 제한 처리
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated()        // URL user : 인증이 되어야 함
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")      // URL manager : 권한 'ROLE_ADMIN', 'ROLE_MANAGER'가 있어야함
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")         // URL admin : 권한 'ROLE_ADMIN'이 있어야함
                .anyRequest().permitAll()      // 위에 명시되지 않은 URL 은 로그인 및 권한 검사 X
                .and()
                .formLogin()
                .loginPage("/loginForm")       // formLogin이 필요한 경우, /login 으로 보낸다.
                .loginProcessingUrl("/login")   // login 주소가 호출이 되면 시큐리티가 낚이채서 대신 로그인을 진행
                .defaultSuccessUrl("/");        // login 성공 시, 보내줄 기본 url


    }
}

 

 

2. User Entity

JPA에 대해 생소하신 분은 이해가 안가실 수 있으니, 위에 남긴 강의를 직접 들어보시는 것을 추천드립니다.

package com.cos.security1.model;

import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.sql.Timestamp;

@Entity(name="tb_user")
@Data
public class User {
    @Id @GeneratedValue
    private Long id;

    private String username;
    private String password;
    private String email;
    private String role;

    @CreationTimestamp
    private Timestamp createDate;
}

 

 

3. PrincipalDetails

package com.cos.security1.config.auth;

// 시큐리티가 /login url을 낚아채서 로그인을 진행시킴.
// 로그인을 하면 security 전용 session을 만들어준다. (Security ContextHolder)
// 오브젝트 타입 => Authentication 타입 객체
// Authentication 안에 User 정보가 있어야 한다.
// User 오브젝트 타입 => UserDetails 타입 객체

import com.cos.security1.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

// Security Session 안에 들어갈 수 있는 객체: Authentication
// Authentication 안에 들어갈 수 있는 객체: UserDetails

public class PrincipalDetails implements UserDetails {

    private User user;

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

    // 해당 유저의 권한을 리턴하는 method
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        String role = user.getRole();

        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return role;
            }
        });

        return collect;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    // 만료 여부
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    // 잠금 여부
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    // 만료 여부
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    // 회원 사용 여부
    public boolean isEnabled() {
        return true;
    }
}

 

4. PrincipalDetailsService

package com.cos.security1.config.auth;

import com.cos.security1.Repository.UserRepository;
import com.cos.security1.model.User;
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;

// Security 설정에서 loginProcessingUrl("/login")으로 걸었기 때문에
// "/login" 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 메소드가 실행된다.
@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    // Security Session = Authentication
    // Authentication => UserDetails
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println(username);
        User userEntity = userRepository.findByUsername(username);

        if(userEntity != null){
            return new PrincipalDetails(userEntity);
        }

        return null;
    }
}

 

 

5. IndexController(MainController)

package com.cos.security1.controller;

import com.cos.security1.Repository.UserRepository;
import com.cos.security1.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
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;

@Controller
public class IndexController {

    @Autowired
    UserRepository userRepository;

    @Autowired
    BCryptPasswordEncoder encoder;

    ...
    
    // 회원가입 로직
    @PostMapping({"/join"})
    public String join(User user){
        user.setRole("ROLE_ADMIN");

        // 패스워드 암호화를 진행하지 않으면 시큐리티 로그인이 불가능
        String encPw = encoder.encode(user.getPassword());
        user.setPassword(encPw);

        userRepository.save(user);

        return "redirect:/loginForm";
    }

    @GetMapping({"/joinProc"})
    @ResponseBody
    public String joinProc(){
        return "회원가입 완료";
    }

    @Secured("ROLE_ADMIN")          // 이 메소드에 대해서만 특정 권한이 필요할 때 사용 가능
    @GetMapping("/info")
    @ResponseBody
    public String info(){
        return "개인정보";
    }

    @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")       // 메소드 접근 이전에 확인하는 로직
    //@PostAuthorize()                                                      // 메소드 접근 이후에 확인하는 로직
    @GetMapping("/data")
    @ResponseBody
    public String data(){
        return "데이터정보";
    }
}

 

반응형