SPRING #8 - SECURITY

김형우·2022년 4월 5일
0

Spring #2

목록 보기
8/8

SPRING - SECURITY

  • SecurityConfig -> 화면 -> ServiceImpl 성공하면 세션에 저장 -> Controller

주소체계

  • 홈화면(로그인 전에)
    127.0.0.1:9090/ROOT/security_home
    127.0.0.1:9090/ROOT/member/security_join
    127.0.0.1:9090/ROOT/member/security_login

  • 관리자 홈화면(로그인 후에)
    127.0.0.1:9090/ROOT/security_admin/home
    127.0.0.1:9090/ROOT/security_admin/insert

  • 판매자 홈화면(로그인 후에)
    127.0.0.1:9090/ROOT/security_seller/home
    127.0.0.1:9090/ROOT/security_seller/item

  • 고객 홈화면(로그인 후에)
    127.0.0.1:9090/ROOT/security_customer/home
    127.0.0.1:9090/ROOT/security_customer/mypage

0. 라이브러리 설치

<dependency>
	<groupId>org.springframework.boot</groupId>
  	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

1. 환경설정

1-1. /config/SecurityConfig.java

  1. Security 라이브러리 설치 후 모든 페이지의 접근권한이 막힘
    : 로그인 페이지로 가는데 내가만든 페이지가 아니고 자체 로그인페이지가 뜸
    : 여기서 설정을 해야함
    : extends = 상속을 받음, 특정기능을 오버라이드 해서 사용하면 됨
  2. 1,2,3 단계 진행후 mService에 uemail, upw, uname, urole이 담겨서 넘어옴
  3. 오버라이드
    : protected void configure(HttpSecurity http) throws Exception
    : extendsWebSecurityConfigurerAdapter가 부모 클래스고,
    : configure(HttpSecurity http) 메소드를 호출해서
    : super = 부모의 기능을 그대로 다 사용한다는 뜻
  4. 각 주소마다(페이지별) 접근 권한 부여
  5. 로그인페이지 설정
    : POST맵핑은 하지 않음
    : 로그인 화면 html에서 설정한 name값을 사용(uemail, upw)
    : 로그인 정보 입력 한 데이터는 바로 서비스로 감
  6. 로그아웃페이지 설정
    : url에 맞게 POST로 호출하면 됨
  7. 접근권한 불가 페이지
  8. h2-console DB 사용을 위해서 임시로
    : http.csrf().disable();
    : http.headers().frameOptions().sameOrigin();
package com.example.config;

import com.example.handler.MyLoginSuccessHandler;
import com.example.handler.MyLogoutSuccessHandler;
import com.example.service.MemberDetailServiceImpl;

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

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 암호를 여기서 해시하고 확인하는 이유는
    // ServiceImpl에서 User로 리턴 받은 이후에
    // 암호를 비교해서 로그인 처리를 하기때문에
    // 그 이전에 ServiceImpl에서 내가 궂이 비교 할 필요가 없다

    // 1. 직접 만든 DetailServiceImpl 객체 생성
    @Autowired
    MemberDetailServiceImpl mService;

    // 2. 암호화 방법 객체생성, @Bean은 서버구동시 자동으로 객체생성
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 3. 직접만든 DetailServiceImpl에 암호화방법 적용
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(mService).passwordEncoder(bCryptPasswordEncoder());
    }

    // 시큐리티 라이브러리 설치 후 모든 페이지 접근권한 막힘
    // 로그인 페이지로 가는데 내가만든 페이지가 아니고 자체 로그인페이지가 뜸
    // 여기서 설정을 해야함
    // extends = 상속을 받음, 특정기능을 오버라이드 해서 사용하면 됨
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // WebSecurityConfigurerAdapter가 부모 클래스고,
        // configure(HttpSecurity http) 메소드를 호출해서
        // super = 부모의 기능을 그대로 다 사용한다는 뜻

        // 때문에 super를 사용하지 않는다 = 기존기능을 다 빼버린다 는 뜻
        // super.configure(http);

        // 각 주소마다의(페이지 별) 접근 권한 부여
        http.authorizeRequests()
                .antMatchers("/security_admin", "/security_admin/**")
                .hasAnyAuthority("ADMIN")
                .antMatchers("/security_seller", "/security_seller/**")
                .hasAnyAuthority("ADMIN", "SELLER")
                .antMatchers("/security_customer", "/security_customer/**")
                .hasAnyAuthority("CUSTOMER")
                .anyRequest().permitAll(); // 나머지요청은 모두 허용

        // 로그인페이지 설정, 단 POST는 직접 만들지 않음
        // 로그인 화면 html에서 설정한 name값을 사용(uemail, upw)
        // 로그인 정보 입력 한 데이터는 바로 서비스로 감
        http.formLogin()
                .loginPage("/member/security_login")
                .loginProcessingUrl("/member/security_loginaction") // th:action 명
                .usernameParameter("uemail") // 아이디
                .passwordParameter("upw") // 패스워드
                .successHandler(new MyLoginSuccessHandler()) // 성공했을때 명령을 수행
                // .defaultSuccessUrl("/security_home", true) // 성공했을때 가는 페이지
                .permitAll();

        // 로그아웃페이지 설정
        // url에 맞게 POST로 호출하면 됨
        http.logout()
                .logoutUrl("/member/security_logout") // 로그아웃 명령을 받을 주소
                .logoutSuccessHandler(new MyLogoutSuccessHandler()) // 성공했을때 명령을 수행
                // .logoutSuccessUrl("/security_home") // 로그아웃했을때 이동하는 페이지
                .invalidateHttpSession(true) // 세션 초기화
                .clearAuthentication(true) // 권한초기화
                .permitAll();

        // 접근권한 불가 페이지
        http.exceptionHandling().accessDeniedPage("/security_403");

        // h2-console DB 사용을 위해서 임시로
        // 나중에 빼야함
        // http.csrf().disable(); // 보안에 취약
        // http.headers().frameOptions().sameOrigin();
        http.csrf()
                .ignoringAntMatchers("/h2-console/**");
        http.headers().frameOptions().sameOrigin();
    }
}

1-2. /service/MemberDetailServiceImpl.java

  1. implements
    : UserDetailsService 사용 할꺼다
  2. Override
    : loadUserByUsername(String username)
    : 리턴 받을 타입은 UserDetails
  3. 리턴 타입에 맞는 클래스가 있음
    : User(아이디, 암호, 권한)
  4. 권한 부분은 Collection타입
    : 한 아이디에 여러개의 권한이 부여 가능
  5. User 타입으로 리턴하면 알아서 처리함
    : return user;
package com.example.service;

import java.util.Collection;

import com.example.dto.MemberDTO;
import com.example.dto.MyUserDTO;
import com.example.mapper.MemberMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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;

@Service
public class MemberDetailServiceImpl implements UserDetailsService {

    // Mapper 사용하겠다 선언
    @Autowired
    MemberMapper mMapper;

    // UserDetailsService에 설계 된 것을 implments 해서 오버라이드 후 사용
    // 로그인에서 입력하는 정보중에서 아이디를 받음
    // MemberMapper를 이용해서 정보를 가져와서 UserDetails로 리턴
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("=== MemberDetailServiceImpl/username => " + username);
        MemberDTO member = mMapper.memberEmail(username);

        // 권한 꺼내서 String 배열로 만듦
        String[] strRole = { member.getUrole() };
        System.out.println("=== MemberDetailServiceImpl/member => " + member);
        // 권한을 n개 부여 가능하기때문에 Collection 타입으로 받는다
        // String 배열로 만들어진 권한을 Collection<GrantedAuthority> 타입으로 변환함
        Collection<GrantedAuthority> role = AuthorityUtils.createAuthorityList(strRole);
        // 암호를 해시해서 넣어야 비교 가능
        // SecurityConfig에서 설정

        // 아이디, 암호, 권한'들'
        // User user = new User(member.getUemail(), member.getUpw(), role);
        MyUserDTO user = new MyUserDTO(member.getUemail(), member.getUpw(), role, member.getUphone(),
                member.getUname());
        System.out.println("=== MemberDetailServiceImpl/user => " + user);
        return user; // User 모양으로 리턴하면 알아서 로그인처리를 하겠다
    }
}

2. 사용

2-1. /mapper/MemberMapper.java

  • 로그인을 할때 아이디(uemail)와 암호(upw)가 DB의 내용과 일치하는지를 비교해야 하기 때문에 upw도 조회한다.
  • 세션에 담을 내용을 늘릴 수 있기때문에 이름(uname)과 연락처(uphone)도 조회한다
package com.example.mapper;

import com.example.dto.MemberDTO;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface MemberMapper {

        // security login
        // SELECT 컬럼명들 FROM 테이블명 WHERE 조건 AND 조건;
        @Select({ "SELECT UEMAIL, UPW, UNAME, UPHONE, UROLE FROM MEMBER ",
                        " WHERE UEMAIL = #{email}" })
        public MemberDTO memberEmail(
                        @Param(value = "email") String em);

        // login
        // SELECT 컬럼명들 FROM 테이블명 WHERE 조건 AND 조건;
        @Select({ "SELECT UEMAIL, UNAME, UROLE FROM MEMBER ",
                        " WHERE UEMAIL = #{email} AND UPW = #{pw}" })
        public MemberDTO memberLogin(
                        @Param(value = "email") String em,
                        @Param(value = "pw") String pw);

        // 파라미터 여거래 사용 가능 => 명칭부여 = @Param
        // join
        // INSERT INTO 테이블명(컬럼명) VALUES(추가할값);
        @Insert({
                        " INSERT INTO MEMBER ",
                        " (UEMAIL, UPW, UNAME, UPHONE, UROLE, UREGDATE) ",
                        " VALUES(#{obj.uemail}, #{obj.upw}, #{obj.uname}, #{obj.uphone}, #{obj.urole}, CURRENT_DATE) " })
        public int memberJoin(@Param(value = "obj") MemberDTO member);
}

2-2. SecurityController.java

  1. 주소목록을 참고해서 @GetMapping
  2. 회원가입 부분에 암호를 해시해서 사용
    2-1. crypt 객체생성
    : BCryptPasswordEncoder bcrpt = new BCryptPasswordEncoder();
    2-2. 암호를 가져와서 해시 한 후 다시 추가
    : member.setUpw(bcrpt.encode(member.getUpw()));
package com.example.controller;

import javax.servlet.http.HttpSession;

import com.example.dto.MemberDTO;
import com.example.dto.MyUserDTO;
import com.example.mapper.MemberMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class SecurityController {

    // Mapper 사용하겠다 선언
    @Autowired
    MemberMapper mMapper;

    // 세션 사용 선언
    @Autowired
    HttpSession httpSession;

    // 세명의 그룹(관리자, 판매자, 고객)으로 나눠서 각자의 권한을 부여

    ///////////////////////////////////////////////////
    //////////////////// 로그인 전 ////////////////////
    ///////////////////////////////////////////////////

    // 홈
    @GetMapping(value = { "/security_home" })
    public String securityhomeGET(
            Model model,
            @AuthenticationPrincipal MyUserDTO user) {

        if (user != null) {
            System.out.println("=== 아이디 : " + user.getUsername());
            System.out.println("=== 이름 : " + user.getName());
            System.out.println("=== 연락처 : " + user.getUserphone());
            System.out.println("=== 권한 : " + user.getAuthorities().toArray()[0]);
        }
        model.addAttribute("user", user);
        System.out.println("=== user => " + user);
        // model.addAttribute("userid", user.getUsername());
        // model.addAttribute("userrole", user.getAuthorities().toArray()[0]);
        // 세션에 담긴 로그인한 사용자의 정보를 꺼내옴
        return "/security/home";
    }

    // 권한없음 접근불가
    @GetMapping(value = "/security_403")
    public String security403GET() {
        return "/security/security_403";
    }

    // 회원가입 GET
    // 127.0.0.1:9090/ROOT/member/security_join
    @GetMapping(value = "/member/security_join")
    public String securityJoinGET() {
        return "/security/join";
    }

    // 회원가입 POST
    @PostMapping(value = "/member/security_join")
    public String securityJoinPOST(
            @ModelAttribute MemberDTO member) {
        // crypt 객체생성
        BCryptPasswordEncoder bcrpt = new BCryptPasswordEncoder();
        // 암호를 가져와서 해시 한 후 다시 추가
        member.setUpw(bcrpt.encode(member.getUpw()));
        member.setUrole("CUSTOMER");

        int ret = mMapper.memberJoin(member);
        System.out.println("member => " + member);
        // System.out.println("ret => " + ret);
        if (ret == 1) {
            // redirect는 주소를 변경한후에 엔터키를 누름
            return "redirect:/security_home";
        }

        // redirect는 주소 변경 후 엔터 누르는 것과 같은 효과
        return "redirect:/member/security_join";
    }

    // 로그인 GET
    @GetMapping(value = "/member/security_login")
    public String securityLoginGET() {
        return "/security/login";
    }

    ///////////////////////////////////////////////////
    //////////////////// 로그인 후 ////////////////////
    ///////////////////////////////////////////////////

    //////////////////// 관리자 ////////////////////
    // 관리자 홈화면(로그인 후)
    // 127.0.0.1:9090/ROOT/security_admin/home
    @GetMapping(value = { "/security_admin/home" })
    public String securityAdminhomeGET() {
        return "/security/admin_home";
    }

    //////////////////// 판매자 ////////////////////
    // 판매자 홈화면(로그인 후)
    // 127.0.0.1:9090/ROOT/security_seller/home
    @GetMapping(value = { "/security_seller/home" })
    public String securitySellerhomeGET() {
        return "/security/seller_home";
    }

    //////////////////// 고객 ////////////////////
    // 고객 홈화면(로그인 후)
    // 127.0.0.1:9090/ROOT/security_customer/home
    @GetMapping(value = { "/security_customer/home" })
    public String securityCustomerhomeGET() {
        return "/security/customer_home";
    }
}

3. 상속

  • 임의의 User 테이블을 만든 후 원하는 컬럼을 추가해서 넣고 세션으로 던진다.

3-1. /dto/MyUserDTO.java

package com.example.dto;

import java.util.Collection;

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

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
public class MyUserDTO extends User {

    private String username = null;
    private String password = null;
    private String userphone = null;
    private String name = null;

    public MyUserDTO(
            String username,
            String password,
            Collection<? extends GrantedAuthority> authorities,
            String userphone,
            String name) {
        // 상속받아도 부모가 수행하던것은 똑같이 해야한다
        // 그리고 추가 한다
        super(username, password, authorities);
        this.username = username;
        this.password = password;
        this.userphone = userphone; // 연락처
        this.name = name; // 이름
    }
}
profile
The best

0개의 댓글