ABOUT ME

Today
Yesterday
Total
  • [Spring] ์Šคํ”„๋ง ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ with Spring Security
    Spring 2023. 9. 14. 22:29
     ๐Ÿ“Œ ์š”์•ฝ
    - BCryptPasswordEncoder๋ฅผ ํ†ตํ•ด ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”ํ•˜์—ฌ ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰.
    - Spring Security๋ฅผ ์ด์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ ๊ตฌํ˜„

     

    ๊ฐ„๋‹จํ•˜๊ฒŒ ์Šคํ”„๋ง์œผ๋กœ ๊ฒŒ์‹œํŒ์„ ๋งŒ๋“ค๋ฉด์„œ, ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. 

     

    1. ํ™˜๊ฒฝ ์…‹ํŒ…

    • spring boot 
    • Project - Gradle
    • Spring boot - 2.7.15
    • Jar
    • java - 11
    • MariaDB
    • JSP
    • JPA
    • Lombok
    • Spring Security

     

     

    2. ํšŒ์›๊ฐ€์ž… ๊ตฌํ˜„

    [๋กœ์ง ์š”์•ฝ]

    1. ์œ ์ €๋กœ๋ถ€ํ„ฐ, โ€œ์•„์ด๋””โ€, โ€œํŒจ์Šค์›Œ๋“œโ€, โ€œ๋‹‰๋„ค์ž„โ€ ์ž…๋ ฅ ๋ฐ›๊ธฐ
    2. ์œ ํšจ์„ฑ ์ฒดํฌ ํ›„ ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ
    3. ํšŒ์›๊ฐ€์ž… ๋กœ์ง ์‹คํ–‰
    4. ์•„์ด๋”” ์ค‘๋ณต ๋ฐœ๊ฒฌ ์‹œ โ€œ์ค‘๋ณต alert ์ฐฝ ํ‘œ์‹œ. ๋ฌธ์ œ ์—†์œผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™.

     

    1) ์œ ์ €๋กœ๋ถ€ํ„ฐ, โ€œ์•„์ด๋””โ€, โ€œํŒจ์Šค์›Œ๋“œโ€, โ€œ๋‹‰๋„ค์ž„โ€ ์ž…๋ ฅ ๋ฐ›๊ธฐ

    <form id="joinForm" class="space-y-6" action="/users/join" method="POST">
                    <div>
                        <label for="loginId" class="block text-sm font-medium leading-6 text-gray-900">์•„์ด๋””</label>
                        <div class="mt-2">
                            <input id="loginId"
                                   name="loginId"
                                   type="text"
                                   class="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                            >
                            <form:errors path="loginId"/>
                        </div>
                    </div>
    
                    <div>
                        <label for="password" class="block text-sm font-medium leading-6 text-gray-900">ํŒจ์Šค์›Œ๋“œ</label>
                        <div class="mt-2">
                            <input id="password" name="password" type="password" required class="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
                        </div>
                    </div>
    
                    <div>
                        <label for="username" class="block text-sm font-medium leading-6 text-gray-900">๋‹‰๋„ค์ž„</label>
                        <div class="mt-2">
                            <input id="username" name="username" type="text" required class="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
                        </div>
                    </div>
    
                    <div>
                        <button type="button"
                                id="joinBtn"
                                onclick="join()"
                                class="flex w-full justify-center rounded-md bg-indigo-500 px-3 py-3 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                            ํšŒ์›๊ฐ€์ž…
                        </button>
                    </div>
                    
    </form>

     

     

    2) ์œ ํšจ์„ฑ ์ฒดํฌ ํ›„ ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ

    const join = async ()=>{
            const loginId = $("#loginId").val();
            const password = $("#password").val();
            const username = $("#username").val();
    
            // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
            if(!checkValidation(loginId, password, username)){
                return;
            }
    
            // api ์ง„ํ–‰
            const requestBody = {
                loginId,
                password,
                username
            }
            const resultStatus = await proceedJoinAPI(requestBody);
            if(resultStatus ==200){
                alert("ํšŒ์›๊ฐ€์ž… ์ถ•ํ•˜๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.")
                location.href="/users/login"
            }else if(resultStatus==409){
                alert("์•„์ด๋””๊ฐ€ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์•„์ด๋””๋กœ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.")
            }
    
    }
    
    const checkValidation = (loginId, password, username)=>{
            const regIdPw = /^[a-zA-Z0-9]{4,12}$/;
            const regName = /^[๊ฐ€-ํžฃa-zA-Z0-9]{2,15}$/;
    
            if (!regIdPw.test(loginId)) {
                alert("์•„์ด๋””๋Š” ์ˆซ์ž, ์˜๋ฌธ 4๊ธ€์ž ์ด์ƒ 12์ž ์ดํ•˜๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
                return false;
            }
            if (!regIdPw.test(password)) {
                alert("ํŒจ์Šค์›Œ๋“œ ์ˆซ์ž, ์˜๋ฌธ 4๊ธ€์ž ์ด์ƒ 12์ž ์ดํ•˜๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
                return false;
            }
            if (!regName.test(username)) {
                alert("๋‹‰๋„ค์ž„์€ ์ˆซ์ž, ์˜๋ฌธ, ํ•œ๊ธ€ 2๊ธ€์ž ์ด์ƒ 15๊ธ€์ž ์ดํ•˜๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
                return false;
            }
            return true;
    
    }
    
    const proceedJoinAPI = async (requestBody)=>{
            const resp = await fetch("http://localhost:8080/users/join", {
                method:"POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(requestBody)
            });
            return resp.status
    }
    • ์•„์ด๋””, ํŒจ์Šค์›Œ๋“œ, ๋‹‰๋„ค์ž„์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ง„ํ–‰
    • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ํ†ต๊ณผ๋˜์ง€ ๋ชปํ•˜๋ฉด alert ํ‘œ์‹œ
    • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ํ†ต๊ณผ๋œ ํ›„, ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ
      - request body์— loginId, password, username ์ „๋‹ฌ

     

    3) ํšŒ์›๊ฐ€์ž… ๋กœ์ง ์‹คํ–‰

    controller

    @PostMapping("/users/join")
    @ResponseBody
    public ResponseEntity<String> join(@RequestBody UserJoinDto userJoinDto){
        log.info(userJoinDto.toString());
    
        // ์ค‘๋ณต ๊ฒ€์‚ฌ
        if(userService.existsDuplicatedUser(userJoinDto)){
           log.info("์ค‘๋ณต ์กด์žฌ");
           return new ResponseEntity<>("fail", HttpStatus.CONFLICT);
        }
    
    	  userService.join(userJoinDto);
        return new ResponseEntity<>("success", HttpStatus.OK);
    }
    • โ€œ/users/joinโ€ API ํ˜ธ์ถœํ•˜์—ฌ, ์œ„์˜ ์ปจํŠธ๋กค๋Ÿฌ ๋กœ์ง ์‹คํ–‰
    • @RequestBody :  request body์— ์˜จ data๋ฅผ ๊บผ๋‚ด์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์• ๋…ธํ…Œ์ด์…˜
    • userService.existsDuplicatedUser(userJoinDto) : ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋‹ด๊ฒจ์žˆ๋Š” ์„œ๋น„์Šค ์ธต์—์„œ, ์œ ์ € ๋กœ๊ทธ์ธ ์•„์ด๋””๊ฐ€ ์ค‘๋ณต๋˜์—ˆ๋Š”์ง€ ํ™•์ธ.
    • userService.join(userJoinDto) : ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋‹ด๊ฒจ์žˆ๋Š” ์„œ๋น„์Šค ์ธต์—์„œ, ์œ ์ €์˜ ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰.

     

    service

    @Service
    @Transactional(readOnly = true)
    @RequiredArgsConstructor
    public class UserService {
        private final UserRepository userRepository;
        private final BCryptPasswordEncoder encoder;
    
        @Transactional
        public void join(UserJoinDto userJoinDto){
            String encodedPassword = encoder.encode(userJoinDto.getPassword());
            userRepository.save(userJoinDto.toEntity(encodedPassword));
        }
    
        public boolean existsDuplicatedUser(UserJoinDto userJoinDto){
            // ์•„์ด๋”” ์ค‘๋ณต ์ฒดํฌ
            if(userRepository.existsByLoginId(userJoinDto.getLoginId())){
                return true;
            }
            return false;
        }
    }
    • BCryptPasswordEncoder : ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” encoder. ์•„๋ž˜์—์„œ ์„ค๋ช…ํ•  Spring Security ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•ด ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Security๊ฐ€ ์ œ๊ณตํ•˜๋Š” BcryptPasswordEncoder๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    • existsDuplicatedUser : repository ์—์„œ existsByLoginId๋ฅผ ์ด์šฉํ•˜์—ฌ login ์•„์ด๋””๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ์ฒดํฌํ•˜๋Š” ํ•จ์ˆ˜
    • join : ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•˜๊ณ  entity ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ ํ›„ repository์— user ์ €์žฅ. (UserJoinDto ๋ฅผ User ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋กœ์ง์€ builder()๋ฅผ ํ†ตํ•ด ์ง„ํ–‰)

     

    repository

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        ...
        Boolean existsByLoginId(String loginId);
    }

    Boolean existsByLoginId(String loginId) : User Table์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜์–ด์˜จ loginId ๊ฐ’๊ณผ ์ผ์น˜ํ•œ loginId๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜. (์ถ”์ƒํ™”๊ฐ€ ์ž˜๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ง์ ‘ ๊ตฌํ˜„์•ˆํ•ด๋„ ๋จ)

     

     

     

    3.  ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„

     

     

     

    ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์— ๋“ค์–ด๊ฐ€๊ธฐ ์•ž์„œ ๋จผ์ €, Spring Security์— ๋Œ€ํ•ด ์•Œ๊ณ  ๊ฐ€์•ผํ•œ๋‹ค. 

    ๐Ÿ“ [Spring Security]

    spring security๋Š” ์Šคํ”„๋ง ๊ธฐ๋ฐ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ณด์•ˆ(์ธ์ฆ, ์ธ๊ฐ€)์„ ๋‹ด๋‹นํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ.

    filter ํ๋ฆ„์— ๋”ฐ๋ผ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒŒ ํŠน์ง•์ด๋‹ค.

    • ์ธ์ฆ(Authentication) : ์‚ฌ์šฉ์ž๊ฐ€ โ€˜์ธ์ฆโ€™ํ•˜๋Š” ๊ณผ์ • โ†’ ๋กœ๊ทธ์ธ ๊ณผ์ • ์ฒ˜๋ฆฌ
    • ์ธ๊ฐ€(Authorization) : ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ๊ถŒํ•œ ๋ถ€์—ฌ

    spring security์— ๋Œ€ํ•œ ํŠน์ • ์…‹ํŒ… ์—†์ด, ์‹œ์ž‘ํ•˜๋ฉด ๋ชจ๋“  ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์— ๋Œ€ํ•ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๊ฐ•์ œ์ ์œผ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ, ํŽ˜์ด์ง€๋ณ„ ๊ถŒํ•œ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

     

    ๋จผ์ € SecurityConfig ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์ž.

     

    // SecurityConfig
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig  {
        // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” encoder 
    		@Bean
        public BCryptPasswordEncoder encoder(){
            return new BCryptPasswordEncoder();
        }
    
        // ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์œ ์ €๋“ค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL
        private static final String[] anonymousUserUrl = {"/users/login", "/users/join"};
    
        // ๋กœ๊ทธ์ธํ•œ ์œ ์ €๋“ค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL
        private static final String[] authenticatedUserUrl = {"/boards/write","/boards/**/**/edit", "/boards/**/**/delete", "/likes/**", "/users/boards/**"};
        
    		@Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity
                    .csrf().disable()
                    .cors().and()
    				// ํŽ˜์ด์ง€๋ณ„ ๊ถŒํ•œ ์ฒ˜๋ฆฌ
                    .authorizeRequests()
                    .antMatchers(anonymousUserUrl).anonymous()
                    .antMatchers(authenticatedUserUrl).authenticated()
                    .anyRequest().permitAll()
                    .and()
                    .exceptionHandling()
                    .and()
                    .build();
        }
    
    }
    • authorizeRequests() : URL์— ๋”ฐ๋ฅธ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์œ„ํ•ด ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์†Œ๋“œ
    • antMatchers(anonymousUserUrl).anonymous() : anonymousUserUrl ํŽ˜์ด์ง€์— ๋Œ€ํ•ด์„œ๋Š” ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์œ ์ €๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ.
    • antMatchers(authenticatedUserUrl).authenticated() : authenticatedUserUrl์— ๋Œ€ํ•ด์„œ๋Š” ๋กœ๊ทธ์ธํ•œ ์œ ์ €๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ.
    • anyRequest().permitAll() : ํŠน์ • URL์„ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ URL์— ๋Œ€ํ•ด์„œ๋Š” ์ „๋ถ€ ์ธ๊ฐ€ํ•ด์คŒ.

     

     

    [๋กœ๊ทธ์ธ ๋กœ์ง ์š”์•ฝ]

    1) ์œ ์ €๋กœ๋ถ€ํ„ฐ โ€˜์•„์ด๋””โ€™์™€ โ€˜ํŒจ์Šค์›Œ๋“œโ€™ ์ž…๋ ฅ ๋ฐ›์Œ

    2) form action์„ ํ†ตํ•ด loginId, password ์ „๋‹ฌ

    3) spring security๊ฐ€ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ์ˆ˜ํ–‰

     

     

    1) ์œ ์ €๋กœ๋ถ€ํ„ฐ โ€˜์•„์ด๋””โ€™, โ€˜ํŒจ์Šค์›Œ๋“œโ€™ ์ž…๋ ฅ๋ฐ›์Œ.

    <form class="space-y-6" method="POST">
                    <div>
                        <label for="loginId" class="block text-sm font-medium leading-6 text-gray-900">์•„์ด๋””</label>
                        <div class="mt-2">
                            <input id="loginId" name="loginId" type="text" required class="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
                        </div>
                    </div>
                    <div>
                        <label for="password" class="block text-sm font-medium leading-6 text-gray-900">ํŒจ์Šค์›Œ๋“œ</label>
                        <div class="mt-2">
                            <input id="password" name="password" type="password" required class="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
                        </div>
                    </div>
    
                    <div>
                        <button type="submit"
                                class="flex w-full justify-center rounded-md bg-indigo-500 px-3 py-3 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                            ๋กœ๊ทธ์ธ
                        </button>
                    </div>
    </form>

    ๋‚˜๋Š” ์—ฌ๊ธฐ์„œ ์•„์ด๋””์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ํ‚ค ๊ฐ’์„ 'loginId', ๋น„๋ฐ€๋ฒˆํ˜ธ์— ํ•ด๋‹นํ•˜๋Š” ํ‚ค ๊ฐ’์„ 'password'๋กœ ์ง€์ •ํ•˜์˜€๋‹ค.

     

     

    2. form action์„ ํ†ตํ•ด loginId, password ์ „๋‹ฌ

    ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ, form action ์ง„ํ–‰

     

     

    3. Spring Security๊ฐ€ ๋กœ๊ทธ์ธ ์ง„ํ–‰

    Spring Security๋ฅผ ์ด์šฉํ•˜๋ฉด, form action์„ ํ†ตํ•ด ์ง„ํ–‰ํ•˜๋Š” ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ๊ธฐ๋Šฅ์„ Spring Security์— ์œ„์ž„ํ•  ์ˆ˜ ์žˆ๋‹ค.

     

    // SecurityConfig
    
    package com.grampus.commnuity.config;
    
    
    import com.grampus.commnuity.config.auth.LoginSuccessHandler;
    import com.grampus.commnuity.repository.UserRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig  {
        @Bean
        public BCryptPasswordEncoder encoder(){
            return new BCryptPasswordEncoder();
        }
    
        private final UserRepository userRepository;
    
        // ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์œ ์ €๋“ค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL
        private static final String[] anonymousUserUrl = {"/users/login", "/users/join"};
    
        // ๋กœ๊ทธ์ธํ•œ ์œ ์ €๋“ค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL
        private static final String[] authenticatedUserUrl = {"/boards/write","/boards/**/**/edit", "/boards/**/**/delete", "/likes/**", "/users/boards/**", "/users/edit", "/users/delete"};
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            return httpSecurity
                    .csrf().disable()
                    .cors().and()
                    // url ๋ณ„ ๊ถŒํ•œ ์ฒ˜๋ฆฌ
                    .authorizeRequests()
                    .antMatchers(anonymousUserUrl).anonymous()
                    .antMatchers(authenticatedUserUrl).authenticated()
                    .antMatchers("/users/admin/**").hasAuthority("ADMIN")
                    .anyRequest().permitAll()
                    .and()
                    .exceptionHandling()
                    .and()
                    // ํผ ๋กœ๊ทธ์ธ
                    .formLogin()
                    .loginPage("/users/login")      // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
                    .usernameParameter("loginId")   // ๋กœ๊ทธ์ธ์— ์‚ฌ์šฉ๋  id
                    .passwordParameter("password")  // ๋กœ๊ทธ์ธ์— ์‚ฌ์šฉ๋  password
                    .failureUrl("/users/login?fail")         // ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ redirect ๋  URL => ์‹คํŒจ ๋ฉ”์„ธ์ง€ ์ถœ๋ ฅ
                    .successHandler(new LoginSuccessHandler(userRepository))
                    .and()
                    // ๋กœ๊ทธ์•„์›ƒ
                    .logout()
                    .logoutUrl("/users/logout")     // ๋กœ๊ทธ์•„์›ƒ URL
                    .invalidateHttpSession(true).deleteCookies("JSESSIONID")
                    .and()
                    .build();
        }
    
    }
    • formLogin() : form ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
    • loginPage("/users/login") : ๋กœ๊ทธ์ธํ•  ํŽ˜์ด์ง€
    • usernameParameter("loginId") : ๋กœ๊ทธ์ธ์— ์‚ฌ์šฉ๋  ์•„์ด๋””์— ๋Œ€ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ KEY ๊ฐ’์„ ๊ธฐ์ž…
    • passwordParameter("password") : ๋กœ๊ทธ์ธ์— ์‚ฌ์šฉ๋  ๋น„๋ฐ€๋ฒˆํ˜ธ์— ๋Œ€ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ KEY ๊ฐ’์„ ๊ธฐ์ž…
    • failureUrl("/users/login?fail") : ๋กœ๊ทธ์ธ ์‹คํŒจ์‹œ redirect ํ•˜๋Š” url
    • successHandler(new LoginSuccessHandler(userRepository)) : ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ, ์‹คํ–‰๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ

    ์ฆ‰, ์œ„์˜ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด์„์ด ๋œ๋‹ค. 

    โ€œ/users/loginโ€ ํŽ˜์ด์ง€์—์„œ form์„ ํ†ตํ•ด ์•ก์…˜์ด ๋“ค์–ด์˜ค๋ฉด,
    ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ์•„์ด๋””(loginId)์™€ ํŒจ์Šค์›Œ๋“œ(password)๋ฅผ ๊ฐ€์ ธ์™€ ๋กœ๊ทธ์ธ์„ ์‹คํ–‰.
    ๋กœ๊ทธ์ธ ์„ฑ๊ณต์‹œ, LoginSuccessHandler()๊ฐ€ ์‹คํ–‰๋˜๋ฉฐ, ๋กœ๊ทธ์ธ ์‹คํŒจ์‹œ, โ€œurl/login?failโ€๋กœ ์ด๋™์ด ๋œ๋‹ค.

     

    ์ถ”๊ฐ€์ ์œผ๋กœ, ์ง์ ‘ db์—์„œ ํšŒ์›์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€ ๋กœ๊ทธ์ธ์ด ์‹คํ–‰๋˜๋„๋ก ํ•ด์•ผ ํ•จ์œผ๋กœ, 

    UserDetailsService์—์„œ loadUserByUsername๋ฅผ ์˜ค๋ฒ„๋กœ๋“œ ํ•ด์ค€๋‹ค.

     

    @Service
    @RequiredArgsConstructor
    public class UserDetailService implements UserDetailsService {
        private final UserRepository userRepository;
        // db์—์„œ ํšŒ์›์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• .
        @Override
        public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
            User user = userRepository.findByLoginId(loginId).orElseThrow(()-> {
                return new UsernameNotFoundException("ํ•ด๋‹น ์œ ์ €๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
            });
            return new UserDetail(user);
        }
    }

     

    public class UserDetail implements UserDetails {
        private User user;
    
        public UserDetail(User user) {
            this.user = user;
        }
    
        // ๊ณ„์ •์ด ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ถŒํ•œ ๋ชฉ๋ก return
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Collection<GrantedAuthority> collection = new ArrayList<>();
            collection.add(() -> {
                return user.getRole().toString();
            });
            return collection;
        }
    
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        @Override
        public String getUsername() {
            return user.getLoginId();
        }
    
        // ๊ณ„์ •์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ (true: ๋งŒ๋ฃŒ X)
        @Override
        public boolean isAccountNonExpired() { return true; }
    
        // ๊ณ„์ •์ด ์ž ๊ฒผ๋Š”์ง€ (true : ์ž ๊น€ X)
        @Override
        public boolean isAccountNonLocked() { return true; }
    
        // ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ (ture: ๋งŒ๋ฃŒ X)
        @Override
        public boolean isCredentialsNonExpired() { return true; }
    
        // ๊ณ„์ •์ด ํ™œ์„ฑํ™”(์‚ฌ์šฉ ๊ฐ€๋Šฅ)์ธ์ง€ (true: ํ™œ์„ฑํ™”)
        @Override
        public boolean isEnabled() { return true; }
    
    }

     

     

     

    Spring Security๋ฅผ ์ด์šฉํ•˜์—ฌ ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. 

    ๋ณดํ†ต ๋กœ๊ทธ์ธ ๋ฐฉ์‹์„ ๊ตฌํ˜„ํ•  ๋•Œ, ์„ธ์…˜, ์ฟ ํ‚ค, jwt ํ† ํฐ์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ,

    ์„ธ์…˜๊ณผ ์ฟ ํ‚ค๋Š” ๋ฐฉ๋ฒ•์ด ์œ ์‚ฌํ•˜๋ฉฐ, ์œ„์˜ ๋ฐฉ๋ฒ•์€ ์„ธ์…˜ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ ํ•œ ๊ฒƒ์ด๋‹ค. 

    jwt ํ† ํฐ์€ ์ฃผ๋กœ ์„œ๋ฒ„๊ฐ€ ์ƒํƒœ๋ฅผ ๊ฐ–์ง€ ์•Š์•„๋„ ๋˜๋Š” stateless ํŠน์ง•์ด ์žˆ๋Š”๋ฐ, ์ฃผ๋กœ rest api๋กœ ๊ตฌํ˜„ํ•  ๋•Œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. 

     

    ์—ฌ๊ธฐ์„œ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ rest api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹Œ, jsp๋ฅผ ์ด์šฉํ•˜์—ฌ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋ฐฉ์‹์„ ์ฑผํƒํ•˜์˜€์œผ๋ฏ€๋กœ 

    ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„์„ ์œ„ํ•ด ์„ธ์…˜ ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜์˜€๋‹ค. 

     

    ๋‚˜์ค‘์— ์‹œ๊ฐ„์ด ๋œ๋‹ค๋ฉด jwt ํ† ํฐ ๋ฐฉ์‹๋„ ์•Œ์•„๋ณด๊ฒ ๋‹ค.

     

     

     

     

Designed by Tistory.