Spring - Boot - Security

์ด๋™์–ธยท2024๋…„ 9์›” 13์ผ

new world

๋ชฉ๋ก ๋ณด๊ธฐ
45/62
post-thumbnail

9.13 (๊ธˆ)

1. Spring-Boot Security

1-1. build

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'




1-2. application.yml

logging:
  level:
    com.zaxxer: info
    org.springframework.jdbc: info
    org.springframework.security: trace
    org:
      apache:
        ibatis: debug
        
      




1-3. CustomSecurityConfig.java

package org.demo.springdemo.board.config;

@Configuration
@Log4j2
@EnableMethodSecurity(prePostEnabled = true)
public class CustomSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

        log.info("filterChain..............");

        http.formLogin(config -> {

        });

        http.csrf(config -> {config.disable();});

        return http.build();
    }
}

๐Ÿ‘‰ EnableMethodSecurity์€ controller์—์„œ ํ•ด๋‹น ๊ถŒํ•œ์ด ์žˆ์„๋•Œ ์‚ฌ์šฉํ• ์ˆ˜์žˆ๋„๋ก ์‚ฌ์šฉํ•˜๋Š” @PreAuthorize, @PostAuthorize ์–ด๋…ธํ…Œ์ด์…˜๋“ค์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.

๐Ÿ‘‰ password๋ฅผ Encodeingํ•ด์•ผํ•˜๋ฏ€๋กœ @bean์„ ํ†ตํ•ด ์ •์˜ํ•˜๊ณ , http://localhost:8080/login์— ์ ‘๊ทผํ• ์ˆ˜์žˆ๋„๋ก http.formLogin(config) ์„ค์ •ํ•œ๋‹ค.

๐Ÿ‘‰ ๋ณด์•ˆ์„ ์œ„ํ•ด ๋ณ€๊ฒฝ๋˜๋Š” csrfํ† ํฐ์€ disable๋กœ ๋น„ํ™œ์„ฑํ™”์‹œํ‚จ๋‹ค.




1-4. CustomUserDetailsService.java

package org.demo.springdemo.board.security;

@Component
@Log4j2
public class CustomUserDetailsService implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        log.info("userName: "+ username);

        UserDetails userDetails = User.builder()
                .username(username)
                .password("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i")
                .authorities("ROLE_USER")
                .build();

        return userDetails;
    }
}

๐Ÿ‘‰ ํ•ด๋‹น ํด๋ž˜์Šค๋Š” UserDetailsService๋ฅผ ์ธํ„ฐํŽ˜์ด์Šคํ•˜๊ณ ์žˆ์œผ๋ฏ€๋กœ ๊ด€๋ จ ์ถ”์ƒ๋ฉ”์†Œ๋“œ๋ฅผ ์ƒ์†ํ•˜๋Š”๋ฐ, UserDetails ์˜ ๋ฐ์ดํ„ฐํƒ€์ž…์œผ๋กœ๋œ ๊ฐ์ฒด๋ฅผ builder๋กœ ์ƒ์„ฑํ•˜์—ฌ username, password, authorities๋ฅผ ๊ตฌ์กฐํ™”ํ•œ๋‹ค.

๐Ÿ‘‰ username์€ ์ •ํ•ด์ง„ ๊ทœ์น™์ด ์—†๊ณ , password๋Š” ์ธ์ฝ”๋”ฉ์„ ํ†ตํ•œ ํ•ด์‹œํ•จ์ˆ˜๋กœ 11, authorities๋Š” ๊ถŒํ•œ์ด๋‹ค.

๐Ÿ“Œ userService ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌ์„ฑ




1-5. BoardController.java

package org.demo.springdemo.board.controller;

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {

    private final UploadUtil uploadUtil;

    private final BoardService boardService;

    @PreAuthorize("hasRole('ROLE_USER')")
    @GetMapping("/register")
    public void register(){

    }

    @PreAuthorize("hasRole('ROLE_USER')")
    @PostMapping("/register")
    public String register(BoardRegisterDTO boardRegisterDTO, RedirectAttributes rttr){

        log.info("register: "+ boardRegisterDTO);

        List<String> uploadedFileNames = uploadUtil.upload(boardRegisterDTO.getImages());

        boardRegisterDTO.setFileNames(uploadedFileNames);

        boardService.register(boardRegisterDTO);

        rttr.addFlashAttribute("bno", boardRegisterDTO.getBno());

        return "redirect:/board/list";
    }

    @PreAuthorize("permitAll()")
    @GetMapping("/list")
    public void list(PageRequest pageRequest, Model model) {

        model.addAttribute("result", boardService.list(pageRequest));
    }

    @PreAuthorize("isAuthenticated()") // ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ
    @GetMapping("/read/{bno}")
    public String read(@CookieValue("view")String viewValue, @PathVariable("bno") Long bno, Model model) {

        log.info("Reading board: "+bno);
        log.info("viewValue: "+viewValue);


        boolean existed = false;

        if(viewValue != null){
            existed = Arrays.stream(viewValue.split("%")).anyMatch(str -> str.equals(bno+""));
        }

        if(!existed) {

            log.info("View Count... update........................");

        }

        Optional<BoardReadDTO> result = boardService.get(bno);

        BoardReadDTO boardReadDTO = result.orElseThrow();

        model.addAttribute("board", boardReadDTO);

        return "/board/read";
    }
  
}

๐Ÿ‘‰ PreAuthorize๋ฅผ ํ†ตํ•ด registerํŽ˜์ด์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ROLE_USER์˜ ๊ถŒํ•œ์ด ์žˆ์„๋•Œ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๊ณ , listํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜๋Š”๊ฑด ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก, ๊ฒŒ์‹œ๋ฌผ์„ ์ฝ๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•œ๋‹ค.




1-6. register.html

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/basicLayout.html}">

<div layout:fragment="content">

<h1>Board Register</h1>

    <form id="form1"  method="post" enctype="multipart/form-data"> <!--ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—ฌ๋Ÿฌ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋‰˜์–ด ์ „์†ก / ํŒŒ์ผ์ „์†ก์‹œ ํ•„์ˆ˜-->

    <input type="text" name="title" >

    <input type="text" name="content" >

    <input type="text" name="writer"  th:value="${#authentication.principal.username}" readonly>

    <input type="text" name="tag" >

    <input type="file" name="images" multiple>

        <button class="submitBtn">CLICK</button>

    </form>
</div>


<script th:inline="javascript" layout:fragment="script">

    const auth = [[ ${#authentication.principal} ]] //์‚ฌ์šฉ์ž์˜ ์ •๋ณด

    console.log(auth)



    const form1 = document.querySelector("#form1")

    document.querySelector(".submitBtn").addEventListener("click",e=>{

        e.preventDefault();

        form1.submit();

        form1.reset();

    },false)



</script>

๐Ÿ‘‰ ํ•ด๋‹น registerํŽ˜์ด์ง€์—์„œ๋Š” ๋กœ๊ทธ์ธํŽ˜์ด์ง€์—์„œ ๋กœ๊ทธ์ธํ•œ username์„ ๊ฐ’์œผ๋กœ ๊ฐ€์ ธ์™€์„œ username์— ๊ณ ์ • value๊ฐ’์œผ๋กœ ๋งŒ๋“ ๋‹ค.

๐Ÿ‘‰ scriptํƒœ๊ทธ์—์„œ auth๋ผ๋Š” ๋ณ€์ˆ˜์— ${#authentication.principal} ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธํ•œ user์ •๋ณด๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , value๊ฐ’์—๋Š” ${#authentication.principal.username} ํ•ด๋‹น ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.




1-7. RememberMe

CustomSecurityConfig.java

package org.demo.springdemo.board.config;


@Configuration
@Log4j2
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class CustomSecurityConfig {

    private final DataSource dataSource; // JDBC์—์„œ DB์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค, application.yml์˜ datasource์˜ ๊ฐ’

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

        log.info("filterChain..............");

        http.formLogin(config -> {

        });

        http.csrf(config -> {config.disable();});

        http.rememberMe(config -> {
            config.tokenValiditySeconds(60*60*24*30);
            config.tokenRepository(persistentTokenRepository());
        });
        
         http.logout(config -> { // logout // http://localhost:8080/logout

        });

        return http.build();
    }
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource);
        return repo;
    }
}

๐Ÿ‘‰ Datasource๋ฅผ ์ฃผ์ž…๋ฐ›์•„์„œ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Š” jdbc์™€ ์—ฐ๊ฒฐํ•˜๋Š”๋ฐ ํ•„์š”ํ•˜๋‹ค.
๐Ÿ‘‰ rememberMe๋ฅผ ํ†ตํ•ด์„œ ์„ธ์…˜๊ณผ ๊ฐ™์€ ๊ฐœ๋…์œผ๋กœ ๋กœ๊ทธ์ธ๊ธฐ๋ก์€ ๊ธฐ์–ตํ•˜๋„๋ก ํ•œ๋‹ค.

๐Ÿ‘‰ PersistentTokenRepository ํ†ตํ•ด ๋กœ๊ทธ์ธํ•œ ๊ธฐ๋ก์„ db์— ๊ธฐ๋กํ•˜๋„๋กํ•œ๋‹ค.

๐Ÿ‘‰ logout ๋กœ๊ทธ์•„์›ƒ์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.




2. KakaoLogin

๐Ÿ‘‰ ๋งŒ๋“ค์–ด๋†“์€ ๊ณ„์ •์—์„œ, ๋กœ๊ทธ์ธ on์„ ํ•ด๋†“๊ณ 

๐Ÿ‘‰ ๋‹‰๋„ค์ž„๊ณผ ํ”„๋กœํ•„์‚ฌ์ง„์€ ๋™์˜๊ฐ€ ๊ฐ€๋Šฅํ•œ๋ฐ, ์ด๋ฉ”์ผ์€ ๋ถˆ๊ฐ€๋Šฅํ•ด์ ธ์žˆ๋‹ค. ๊ฐœ์ธ์ •๋ณด๋™์˜ ์‹ฌ์‚ฌ์‹ ์ฒญ์—์„œ ์•ฑ์•„์ด์ฝ˜๋“ฑ๋กํ•˜์—ฌ ํŒŒ์ผ์‚ฌ์ง„์„ ํƒํ›„-> ๋น„์ฆˆ์•ฑ -> ๊ฐœ์ธ๊ฐœ๋ฐœ์ž ๋น„์ฆˆ์•ฑ์ „ํ™˜ - ์ด๋ฉ”์ผํ•„์ˆ˜๋™์˜ ์ „ํ™˜

๐Ÿ‘‰ ํ”Œ๋žซํผ์—์„œ ํ•˜๋‹จ ๋„๋ฉ”์ธ์„ ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •ํ•ด์ฃผ๊ธฐ
๐Ÿ‘‰ ํ•˜๋‹จ์— redirect url ์„ค์ • ๋“ค์–ด๊ฐ€์„œ ํ™œ์„ฑํ™”

๐Ÿ‘‰ url ๋ณต์‚ฌํ•ด๋†“๊ธฐ

๐Ÿ‘‰ ๋ณด์•ˆํ‚ค ๋ณต์‚ฌ

๐Ÿ‘‰ (Rest Key) ๋ณต์‚ฌ

2-0. gradle ์ถ”๊ฐ€

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

2-1. CustomSecurityConfig ์ถ”๊ฐ€

 http.oauth2Login(config -> { // kakaoLogin

        });

2-2. application.yml ์ถ”๊ฐ€



spring:
  application:
    name: springDemo
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    username: malldbuser
    password: malldbuser
    url: jdbc:mariadb://localhost:3306/malldb

    hikari:
      minimum-idle: 1
      maximum-pool-size: 5
      connection-timeout: 10000

  thymeleaf:
    cache: false

  servlet:
    multipart:
      enabled: true
      location: C:\\upload
      max-file-size: 2MB
      max-request-size: 20MB
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: #Rest key
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-authentication-method: client_secret_post
            authorization-grant-type: authorization_code
            scope: profile_nickname, account_email, profile_image
            client-name: Kakao
            client-secret: #๋ณด์•ˆ key
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

logging:
  level:
    com.zaxxer: info
    org.springframework.jdbc: info
    org.springframework.security: trace
    org:
      apache:
        ibatis: debug


mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: org.demo.springdemo.**.dto
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

board:
  upload: C:\\upload\\attach

2-3. SocialUserService

package org.demo.springdemo.board.security;

@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService implements OAuth2UserService {


    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        log.info("---------------");
        log.info("---------------");
        log.info(userRequest);
        log.info("---------------");
        log.info("---------------");

        return null;
    }
}

๐Ÿ‘‰ kakao ๋“ค์–ด๊ฐ€์„œ ๋กœ๊ทธ์ธํ•˜๋ฉด

๐Ÿ‘‰ ํ•ด๋‹น์˜ค๋ฅ˜๋ฐœ์ƒ




2-4. SocialUserService edit

package org.demo.springdemo.board.security;



@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService  extends DefaultOAuth2UserService {


    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        log.info("---------------");
        log.info("---------------");
        log.info(userRequest);

        String serviceName = userRequest.getClientRegistration().getClientName();

        OAuth2User oAuth2User = super.loadUser(userRequest);

        java.util.Map<String, Object> paramMap = oAuth2User.getAttributes();

        paramMap.forEach((k,v) -> {
            log.info("key: "+k+" value: "+v);
        });

        log.info(serviceName); // kakao

        LinkedHashMap accountMap = (LinkedHashMap) paramMap.get("account");

        String email = (String) accountMap.get("email");

        log.info(email);

        return oAuth2User;
    }
}

๐Ÿ‘‰ servicename์€ userRequest์—์„œ ํด๋ผ์ด์–ธํŠธ ๋“ฑ๋ก์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , getClientName์œผ๋กœ ์ธ์ฆ์„œ๋น„์Šค ์ด๋ฆ„์ธ kakao๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
๐Ÿ‘‰ loaduser๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ OAuth2์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๋กœ๋“œํ•˜๊ณ , OAuth2๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ์†์„ฑ์„ map์˜ ํ˜•ํƒœ์ธ ํ‚ค์™€ ๊ฐ’์˜ ํ˜•ํƒœ๋กœ ๊ฐ€์ ธ์˜จ๋‹ค.

๐Ÿ‘‰ ๊ทธ ์†์„ฑ์ค‘์— account์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์„ LinkedHashMap์œผ๋กœ ์บ์ŠคํŒ…ํ•˜์—ฌ ํ• ๋‹นํ•˜๊ณ , email๋˜ํ•œ ๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ ํ• ๋‹นํ•œ๋‹ค.

2-5. MemberDTO

package org.demo.springdemo.board.dto;

@Data
public class MemberDTO implements UserDetails, OAuth2User {

    private String mid;
    private String mpw;
    private String mname;

    private List<String> roles;

    private Map<String, Object> props; // ์‚ฌ์šฉ์ž์ •๋ณด์˜ ์†์„ฑ์„ ์ €์žฅ

    @Override
    public Map<String, Object> getAttributes() {  // ์‚ฌ์šฉ์ž์˜ ์†์„ฑ์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์†Œ๋“œ
        return props;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_"+role)).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return mpw;
    }

    @Override
    public String getUsername() {
        return mid;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String getName() {
        return this.mid;
    }
}

๐Ÿ‘‰ DTO์—์„œ๋Š” props๋ฅผ ๋งŒ๋“ค์–ด map์ธ ํ‚ค์™€๊ฐ’์˜ ํ˜•ํƒœ๋กœ ๋ณ€์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ณ , getAttribute๋ฅผ ํ†ตํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ์†์„ฑ์˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์ •์˜ํ•œ๋‹ค.




2-6. CustomUserDetailsService.java

package org.demo.springdemo.board.security;

@Component
@Log4j2
public class CustomUserDetailsService implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        log.info("userName: "+ username);

        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setMid(username);
        memberDTO.setMpw("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i");
        memberDTO.setRoles(List.of("USER"));

        return memberDTO;
    }
}

๐Ÿ‘‰ ํ•ด๋‹น CustomLogin์€ ์ผ๋ฐ˜์ ์ธ Login์„ ๊ตฌํ˜„ํ•˜์—ฌ ๋งŒ๋“ค์—ˆ๋‹ค.




2-7. SocialUserService.java

package org.demo.springdemo.board.security;

@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService  extends DefaultOAuth2UserService {


    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        log.info("---------------");
        log.info("---------------");
        log.info(userRequest);

        String serviceName = userRequest.getClientRegistration().getClientName();

        OAuth2User oAuth2User = super.loadUser(userRequest);

        java.util.Map<String, Object> paramMap = oAuth2User.getAttributes();

        paramMap.forEach((k,v) -> {
            log.info("key: "+k+" value: "+v);
        });

        log.info(serviceName); // kakao

        LinkedHashMap accountMap = (LinkedHashMap) paramMap.get("account");

        String email = (String) accountMap.get("email");

        log.info(email);

        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setMid(email);
        memberDTO.setMpw("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i");
        memberDTO.setRoles(List.of("USER"));
        memberDTO.setProps(paramMap);

        return oAuth2User;
    }
}

๐Ÿ‘‰ ํ•ด๋‹น์ฝ”๋“œ๋Š” kakao์™€ ๊ฐ™์€ socailLogin๋ฐฉ์‹์„ ์œ„ํ•ด ์ž‘์„ฑ๋˜์—ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€