Spring security 구현

유요한·2023년 12월 1일
0
post-thumbnail

구현

의존성

Spring Boot에 의존성을 추가해 줍니다.

gradle

dependencies {
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
        implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    testImplementation 'org.springframework.security:spring-security-test'
}

maven

<dependencies>
  <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
  </dependency>
</dependencies>

application.yml

spring:
  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true

logging:
  level:
    me.silvernine: debug

설정을 해주고 실행을 하면

암호가 뜹니다. 이 암호으로 로그인해야 정상적으로 뷰가 보입니다.
아이디는 기본적으로 user고 비밀번호는 서버를 실행할 때마다 다릅니다.


여기서 localhost:8080/logout을하면 다음과 같은 창이 나옵니다.

package com.example.jwt_security.controller;


import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/hello")
    // ResponseEntity란, httpentity를 상속받는, 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스이다.
    // ResponseEntity에는 사용자의  HttpRequest에 대한 응답 데이터가 포함된다.
    // 스프링에서 제공하며 header와 body를 포함하고, 추가로 HttpStatus 코드가지 함께 축가할 수 있는 클래스
    // REST 컨트롤러 혹은 일반 컨트롤러에서 응답하는 객체로서 사용된다.
    public ResponseEntity<String> hello() {
        return ResponseEntity.ok("hello");
    }
}

이를 해결하기 위한 Security 설정과 기본적인 Data 설정을 진행하겠습니다.

여기서 먼저 @EnableWebSecurity를 알아보고자 합니다.

@EnableWebSecurity

위의 SecurityConfig에 붙은 @EnableWebSecurity을 보면 WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, HttpSecurityConfiguration.class들을 import해서 실행시켜주는 것을 알 수 있습니다. 해당 annotation을 붙여야지 Securiry를 활성화 시킬 수 있습니다.

그리고 추가적인 설정을 위해서 WebSecurityConfigurer를 implements를 하거나 WebSecurityConfigurerAdapter를 extends하는 방법이 있습니다. 하지만 여기서 WebSecurityConfigurerAdapter에 선줄이 그어져 있는 것을 볼 수 있습니다. 이건 이제는 사용하지 않는다는 겁니다.

스프링 버전이 업데이트 됨에 따라 WebSecurityConfigurerAdapter와 그 외 몇 가지들이 Deprecated 됐습니다.

어떻게 변경되었지?

기존에는 WebSecurityConfigurerAdapter를 상속받아 설정을 오버라이딩 하는 방식이었는데 바뀐 방식에서는 상속받아 오버라이딩하지 않고 모두 Bean으로 등록을 합니다.

  • 스프링 웹 시큐리티를 설정 클래스를 만들려면 @Configuration, @EnableWebSecurity 애노테이션과 WebSecurityConfigurerAdapter 클래스를 상속 받아야 한다.
  • http.authorizeRequests() 메서드로 특정한 경로에 특정한 권한을 가진 사용자만 접근할 수 있도록 설정할 수 있다.
  • mvcMatchers() : 특정 경로를 지정해서 권한 설정 가능
  • formLogin() : 인증이 필요한 요청은 스프링시큐리티에서 사용하는 기본 Form Login Page 사용
  • httpBasic() : http 기본인증 사용

※ antMatchers, mvcMatchers 차이

특정경로 지정해서 권한을 설정할때 antMatchers, mvcMatchers가 있는데 antMatchers는 URL 매핑 할때 개미패턴, mvcMatchers는 mvc패턴이다. antMatchers(”/info”) 하면 /info URL과 매핑 되지만 mvcMatchers(”/info”)는 /info/, /info.html 이 매핑이 가능하다.

변하기전

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UsuariService userDetailsService;

    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;
    
    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .antMatchers("/api/auth/**").permitAll().antMatchers("/api/test/**").permitAll().antMatchers("/api/v1/**").permitAll().anyRequest()
                .authenticated();

        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }

}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓변경↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    UsuariService userDetailsService;

    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Bean
    AuthenticationManager authenticationManager(AuthenticationManagerBuilder builder) throws Exception {
        return builder.userDetailsService(userDetailsService).passwordEncoder(encoder()).and().build();
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/test/**").permitAll()
                .antMatchers("/api/v1/**").permitAll()
                .anyRequest().authenticated();
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

변경전

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
    public void configure(WebSecurity web) {
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                // /about 요청에 대해서는 로그인을 요구함
                .antMatchers("/about").authenticated()
                // /admin 요청에 대해서는 ROLE_ADMIN 역할을 가지고 있어야 함
                .antMatchers("/admin").hasRole("ADMIN")
                // 나머지 요청에 대해서는 로그인을 요구하지 않음
                .anyRequest().permitAll()
                .and()
                // 로그인하는 경우에 대해 설정함
            .formLogin()
                // 로그인 페이지를 제공하는 URL을 설정함
                .loginPage("/user/loginView")
                // 로그인 성공 URL을 설정함
                .successForwardUrl("/index")
                // 로그인 실패 URL을 설정함
                .failureForwardUrl("/index")
                .permitAll()
                .and()
                .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓변경↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

@Configuration
public class SecurityConfiguration {

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
             http.csrf().disable().authorizeRequests()
                // /about 요청에 대해서는 로그인을 요구함
                .antMatchers("/about").authenticated()
                // /admin 요청에 대해서는 ROLE_ADMIN 역할을 가지고 있어야 함
                .antMatchers("/admin").hasRole("ADMIN")
                // 나머지 요청에 대해서는 로그인을 요구하지 않음
                .anyRequest().permitAll()
                .and()
                // 로그인하는 경우에 대해 설정함
            .formLogin()
                // 로그인 페이지를 제공하는 URL을 설정함
                .loginPage("/user/loginView")
                // 로그인 성공 URL을 설정함
                .successForwardUrl("/index")
                // 로그인 실패 URL을 설정함
                .failureForwardUrl("/index")
                .permitAll()
                .and()
                .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

}
@RequiredArgsConstructor
// 기본적인 web 보안을 활성화 하겠다는 의미
@EnableWebSecurity
public class SecurityConfig {

    private final ObjectMapper objectMapper;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

	// 정적 자원(Resource)에 대해서 인증된 사용자가 정적 자원에 대한 
    // 접근에 대해 ‘인가’에 대한 설정을 담당하는 메서드이다.
    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring().mvcMatchers(
                "/v3/api-docs/**",
                "/swagger-ui/**",
                "/api/v1/login" // 임시
        );
    }

	//  해당 메서드 내에서 CustomAuthenticationFilter 호출합니다.
    // HTTP에 대해서 ‘인증’과 ‘인가’를 담당하는 메서드이며
    // 필터를 통해 인증 방식과 인증 절차에 대해서 등록하며 설정을 담당하는 메서드이다.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/api/v1/**").hasAuthority(USER.name())
                        .and()
                .httpBasic().disable()
                .formLogin().disable()
                .cors().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest().permitAll()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

                .exceptionHandling()
                    .authenticationEntryPoint(((request, response, authException) -> {
                        response.setStatus(HttpStatus.UNAUTHORIZED.value());
                        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                        objectMapper.writeValue(
                                response.getOutputStream(),
                                ExceptionResponse.of(ExceptionCode.FAIL_AUTHENTICATION)
                        );
                }))
                    .accessDeniedHandler(((request, response, accessDeniedException) -> {
                        response.setStatus(HttpStatus.FORBIDDEN.value());
                        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                        objectMapper.writeValue(
                                response.getOutputStream(),
                                ExceptionResponse.of(ExceptionCode.FAIL_AUTHORIZATION)
                        );
                    })).and().build();
    }
}
    http.csrf().disable();
        //http.httpBasic().disable(); 
        // 일반적인 루트가 아닌 다른 방식으로 요청시 거절, header에 id, pw가 아닌 token(jwt)을 달고 간다. 그래서 basic이 아닌 bearer를 사용한다.
        http.httpBasic().disable()
                .authorizeRequests()// 요청에 대한 사용권한 체크
                .antMatchers("/test").authenticated()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/**").permitAll()
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class); // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
        // + 토큰에 저장된 유저정보를 활용하여야 하기 때문에 CustomUserDetailService 클래스를 생성합니다.
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
package com.example.shopping_.config;

import com.example.shopping_.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.config.annotation.web.builders.WebSecurity;

@Configuration
@EnableWebSecurity
public class SecurityConfig{

    @Autowired
    MemberService memberService;

    /*
    *       @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                    auth.userDetailsService(memberService)
                        .passwordEncoder(passwordEncoder());
    }
    *       ↓변경
    * */
    // 인증에 대한 인터페이스
    // 인증에 대한 환경설정을 수행합니다.
    // 스프링 시큐리티의 인증은 AuthenticationManager를 통해 이루어지며
    // AuthenticationManagerBuilder가 AuthenticationManager를 생성합니다.
    // userDetailsService를 구현하고 있는 객체로 memberService를 지정해주며
    // 비밀번호 암호화를 위해 passwordEncoder를 지정해줍니다.
    @Bean
    AuthenticationManager authenticationManager(AuthenticationManagerBuilder builder) throws Exception {
        return builder.userDetailsService(memberService).passwordEncoder(passwordEncoder()).and().build();
    }

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

//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.formLogin()
//                .loginPage("/member/login")
//                .defaultSuccessUrl("/")
//                .usernameParameter("email")
//                .failureUrl("/member/login/error")
//                .and()
//                .logout()
//                .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
//                .logoutSuccessUrl("/")
//        ;
//
//        http.authorizeRequests()
//                .mvcMatchers("/", "/member/**", "/item/**", "/images/**").permitAll()
//                .mvcMatchers("/admin/**").hasRole("ADMIN")
//                .anyRequest().authenticated()
//        ;
//
//        http.exceptionHandling()
//                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
//        ;
//    }
//      ↓변경

    // HTTP 인증, 인가
    // HTTP에 대해서 '인증'과 '인가'를 담당하는 메서드이며
    // 필터를 통해 인증 방식과 인증 절차에 대해서 등록하며 설정을 담당하는 메서드
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // formLogin() : 인증이 필요한 요청은 스프링 시큐리티에서 사용하는 기본 Form Login Page 사용
        http.formLogin()
                // 사용자가 로그인 요청을 보내는 주소
                .loginPage("/member/login")
                // 로그인 성공하면 보내지는 기본 페이지를 의미한다.
                .defaultSuccessUrl("/")
                // 로그인 시 사용할 파라미터 이름으로 email을 지정합니다.
                .usernameParameter("email")
                // 로그인 실패하면 보내지는 페이지
                .failureUrl("/member/login/error")
                .and()
                .logout()
                // 로그아웃 url을 설정
                .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
                // 로그아웃 성공시 이동할 URL을 설정
                .logoutSuccessUrl("/")
        ;

        http.csrf().disable();

        // 특정한 경로에 특정한 권한을 가진 사용자만 접근할 수 있도록 아래의 메소드를 이용합니다.
        // 시큐리티 처리에 HttpServletRequest를 이용한다는 것을 의미
        http.authorizeRequests()
                // mvcMatchers : 특정 경로를 지정해서 권한 설정 가능
                // permitAll() : 모든 사용자가 인증(로그인)없이 해당 경로에 접근할 수 있도록 설정
                // 메인 페이지, 회원 관련 URL, 상품 상세 페이지, 상품 이미지를 불러오는 경로가 이에 해당
                .mvcMatchers("/", "/member/**", "/item/**", "/images/**").permitAll()
                // /admin으로 시작되는 경로는 해당 계정이 ADMIN Role일 경우에만 접근 가능하도록 합니다.
                .mvcMatchers("/admin/**").hasRole("ADMIN")
                // permitAll()과 ADMIN일때만 접근할 수있는 요청에 설정한 URL를 제외한
                // 나머지 경로들은 모두 인증을 요구하도록 설정
                .anyRequest().authenticated()
        ;

        // 인증되지 않은 사용자가 리소스에 접근했을 때 수행되는 핸들러를 등록
        http.exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        ;
        return http.build();
    }




//    @Override
//    public void configure(WebSecurity web) throws Exception {
//        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
//        web.httpFirewall(defaultHttpFirewall());
//    }

    // 정적 자원(Resource)에 대해서 인증된 사용자가
    // 정적 자원에 대한 접근에 대해 '인가'에 대한 설정을
    // 담당하는 메서드
    // static 디렉터리의 하위 파일은 인증을 무시하도록 설정
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .antMatchers("/css/**", "/js/**", "/img/**")
                .and()
                .httpFirewall(defaultHttpFirewall());

    }

    @Bean
    public HttpFirewall defaultHttpFirewall() {
        return new DefaultHttpFirewall();
    }


}

스프링 시큐리티의 어노테이션인 @EnableWebSecurity 어노테이션은 기본적으로 CSRF 공격을 방지하는 기능을 지원하고 있습니다. 시큐리티를 적용하면 보통 SecurityFilterChain filterChain(HttpSecurity http) 메서드에는 아래와 같이 csrf().disable()로 적용을 하는데요,

이러한 CSRF란 무엇인지 알아보겠습니다! 😃`

🎯 CSRF란?

사이트 간 요청 위조(Cross-Site Request Forgery)

CSRF란 웹 애플리케이션의 취약점 중 하나로, 이용자가 의도하지 않은 요청을 통한 공격을 의미합니다. 즉 CSRF 공격이란, 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(등록, 수정, 삭제 등)를 특정 웹사이트에 요청하도록 만드는 공격 입니다.

Form Login사용하기

사용은 http.formLogin()을 추가하게 된다면 Form로그인 기능이 작동하게 되며 api들은 다음과 같습니다.

protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
       .loginPage(/login.html")   			// 사용자 정의 로그인 페이지
       .defaultSuccessUrl("/home)			// 로그인 성공 후 이동 페이지
       .failureUrl(/login.html?error=true)	        // 로그인 실패 후 이동 페이지
       .usernameParameter("username")			// 아이디 파라미터명 설정
       .passwordParameter(“password”)			// 패스워드 파라미터명 설정
       .loginProcessingUrl(/login")			// 로그인 Form Action Url
       .successHandler(loginSuccessHandler())		// 로그인 성공 후 핸들러
       .failureHandler(loginFailureHandler())		// 로그인 실패 후 핸들러
}

package com.example.board2.config;

import org.springframework.context.annotation.Bean;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .antMatchers("/css/**", "/js/**", "/img/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // http basic 인증 방법을 비활성화
                .httpBasic().disable()
                // form login을 비활성화
                .formLogin().disable()
                // csf관련 설정을 비활성화
                .csrf().disable()
                // 세션 관리 정책을 설정
                // 세션을 유지하지 않도록 설정해줍니다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 모든 URL에 대해 접근 허용
                .antMatchers("/**").permitAll();

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // PasswordEncoder 구현체로 DelegatingPasswordEncoder를 사용해줍니다.
        // PasswordEncoderFactories 클래스에 팩토리 메소드를 이용하면 인스턴스를 생성할 수 있습니다.
        // 이것을 구현체로 사용한 이유는 비밀번호 암호화를 위한 다양한 알고리즘이 있는데
        // 이 구현체를 이용하면 여러 알고리즘들을 선택적으로 편리하게 사용할 수 있습니다.
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

DelegatingPasswordEncoder를 사용하기로 결정햐였습니다.
PasswordEncoderFactories.createDelegatingPasswordEncoder() 가 어떤 식으로 구현체를 생성하는지 확인하기 위해 코드를 살짝 들여다보겠습니다.

	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

다음과 같이 여러 개의 PasswordEncoder 구현체를 생성하고, DelegatingPasswordEncoder의 인자로 넘겨줍니다.

DelegatingPasswordEncoder의 생성자는 다음과 같습니다.

	public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
			String idPrefix, String idSuffix) {
		if (idForEncode == null) {
			throw new IllegalArgumentException("idForEncode cannot be null");
		}
		if (idPrefix == null) {
			throw new IllegalArgumentException("prefix cannot be null");
		}
		if (idSuffix == null || idSuffix.isEmpty()) {
			throw new IllegalArgumentException("suffix cannot be empty");
		}
		if (idPrefix.contains(idSuffix)) {
			throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix);
		}

		if (!idToPasswordEncoder.containsKey(idForEncode)) {
			throw new IllegalArgumentException(
					"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
			if (id == null) {
				continue;
			}
			if (!idPrefix.isEmpty() && id.contains(idPrefix)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix);
			}
			if (id.contains(idSuffix)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix);
			}
		}
		this.idForEncode = idForEncode; // 1
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
		this.idPrefix = idPrefix;
		this.idSuffix = idSuffix;
	}
  1. 생성자로 넘겨준 idForEncode를 이용하여 기본적으로 사용될 암호화 알고리즘 구현체를 지정하는 것 같습니다. 즉, 여기에선 bcrypt 알고리즘이 사용됩니다.

어떻게 암호화하는지 DelegatingPasswordEncoder.encode 메소드도 살펴보도록 하겠습니다.

@Override
	public String encode(CharSequence rawPassword) {
		return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
	}

encode를 수행할 때, PREFIX와 SUFFIX 사이에 암호화 알고리즘을 기록해놓고, 알고리즘이 적용된 문자열과 함께 반환됩니다. (PREFIX와 SUFFIX는 디폴트로 '{'와 '}'으로 설정되어 있습니다.)


SecurityConfig

@Configuration
// 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다.
@EnableWebSecurity
// secured 어노테이션 활성화, preAuthorize 어노테이션 활성화
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private PrincipalOauth2UserService principalOauth2UserService;


    // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    /*
    *   @Override
    *   protected void cofigure(HttpSecurity http) throws Exception{}
    *
    *   ↓아래와 같이 바뀜↓
    * */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // .csrf() : Cross site Request forgery로 사이즈간 위조 요청인데
        // 즉 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다.
        /*
        *   예를 들어 A라는 도메인에서, 인증된 사용자 H가 위조된
        *   request를 포함한 link, email을 사용하였을 경우(클릭, 또는 사이트 방문만으로도),
        *   A 도메인에서는 이 사용자가 일반 유저인지, 악용된 공격인지 구분할 수가 없다.
        * */
         http.csrf().disable();
         // 특정한 경로에 특정한 권한을 가진 사용자만 접근할 수 있도록 아래의 메소드를 이용합니다.
         http.authorizeRequests()
                 // antMatchers()는 특정한 경로를 지정합니다.
                 // antMatchers 메소드는 요청 타입을 의미
                 // URL이 user뒤에 오는 모든 것을 .authenticated() 메소드가 적용되는데
                 // 해당 메소드는 로그인 된 상태를 의미합니다.
                 // 그러므로 /user/*는 로그인된 상태에서만 접근 가능합니다.
                 .antMatchers("/user/**").authenticated()
                 // hasRole()은 시스템상에서 특정 권한을 가진 사람만이 접근할 수 있음
                 .antMatchers("/manager/**").access(
                         "hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                 .antMatchers("/admin/**").access(
                         "hasRole('ROLE_ADMIN')")
                 .anyRequest().permitAll()
                 .and()
                 .formLogin()
                 .loginPage("/loginForm")
                 // /login 주소가 호출이되면 시큐리티가 낚아채서 대신 로그인을 진행합니다.
                 // 이걸 추가하면 컨트롤러에 /login을 만들지 않아도 된다.
                 .loginProcessingUrl("/login")
                 .defaultSuccessUrl("/")
                 
                   return http.build();

    }
  }  

PrincipalDetails

package com.example.security_jwt.config.auth;

// 시큐리티가 /login을 낚아채서 로그인을 진행시킨다.
// 로그인을 진행이 완료되면 시큐리티 session을 만들어줍니다. (Security ContextHolder)
// 오브젝트 → Authentication 타입 객체
// Authentication 안에 User 정보가 있어야 함
// User 오브젝트 타입 → UserDetails 타입 객체

import com.example.security_jwt.model.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

// Security Session → Authentication → UserDetails(PrincipalDetails)
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final User user;

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

    @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 false;
    }

    @Override
    public boolean isEnabled() {
        // 우리 사이트!! 1년 동안 회원이 로그인을 안하면
        // 휴먼 계정으로 하기로 함
        // 현재 시간 - 로그인 시간 → 1년을 초과하면 return false;

        return true;
    }
}

PrincipalDetailsService

package com.example.security_jwt.config.auth;

import com.example.security_jwt.model.User;
import com.example.security_jwt.repository.UserRepository;
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;

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

    @Autowired
    private UserRepository userRepository;


    // 시큐리티 session = Authentication = UserDetails
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUserName(username);
        if(user != null) {
            return new PrincipalDetails(user);
        }
        return null;
    }
}

REST 방식

package com.example.board3.config.security;

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.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
// 시큐리티 활성화
@EnableWebSecurity
// 예전에는 extends WebSecurityConfigurerAdapter를 햇지만 이제는 지원하지 않음
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder encoder() {
        return  new BCryptPasswordEncoder();
    }


    // HTTP에 대해서 인증과 인가를 담당하는 메서드이며 필터를 통해 인증 방식과 인증 절차에 대해
    // 등록하며 설정을 담당하는 메서드
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws  Exception {
        http
                // CorsFilter라는 필터가 존재하는데 이를 활성화 시키는 작업
                .cors().and()
                // 세션을 사용하지 않고 JWT 토큰을 활용하여 진행하고
                // REST API를 만드는 작업이기 때문에 이 처리를 합니다.
                .csrf().disable()
                // 스프링 시큐리티에서 세션을 관리하지 않겠다는 뜻입니다.
                // 서버에서 관리되는 세션없이 클라이언트에서 요청하는 헤더에 token을
                // 담아보낸다면 서버에서 토큰을 확인하여 인증하는 방식을 사용할 것이므로
                // 서버에서 관리되어야할 세션이 필요없습니다.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                // 인증절차에 대한 설정
                .authorizeRequests()
                // /v1/api/users에 대해 로그인을 요구함
                .antMatchers("/v1/api/users").authenticated()
                .antMatchers("/v1/api/boards").authenticated()
                // 나머지 요청에 대해서는 로그인을 요구하지 않음
                .anyRequest().permitAll()

                .and()
                // formLogin 기능을 끈다고 해서 form 태그 내에 로그인 기능을 못쓴다는 것은 아닙니다.
                // formLogin을 끄면 초기 로그인 화면이 사라집니다.
                // 그것보다 궁극적인 이유는 아래에 설명할 JWT의 기능을 만들기 위해서 입니다.
                // formLogin은 세션 로그인 방식에서 로그인을 자동처리 해준다는 장점이 존재했는데,
                // JWT에서는 로그인 방식 내에 JWT 토큰을 생성하는 로직이 필요하기 때문에
                // 로그인 과정을 수동으로 클래스를 만들어줘야 하기 때문에 formLogin 기능을 제외 합니다.
                // formLogin 기능 자체가 REST API에 반대되는 특징을 가지고 있습니다.
                // formLogin의 defaultSuccessUrl 메소드로 로그인 성공 시 리다이렉트 할 주소를 입력하게 되는데
                // REST API에서는 서버가 페이지의 기능을 결정하면 안되기 때문에 결과적으로 필요하지 않은 formLogin은 disable합니다.
                .formLogin().disable()
                // httpBasic은 기본적으로 disable이지만 켜두면 위와 같이 알림창이 뜹니다.
                // 쿠키와 세션을 이용한 방식이 아니라 request header에 id와 password값을 직접 날리는 방식이라
                // 보안에 굉장히 취약합니다. REST API에서는 오로지 토큰 방식을 이용하기 때문에 보안에 취약한
                // httpBasic 방식은 해제한다고 보시면 됩니다.
                .httpBasic().disable();

        return http.build();
    }


    // 정적 자원(Resource)에 대해서 인증된 사용자가 정적 자원에 대해 '인가'에
    // 대한 설정을 담당하는 메서드
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return
                (web) -> web.ignoring().antMatchers("/images/**", "/js/**");
    }



}

@EnableGlobalMethodSecurity

Spring Security에서 메서드 수준의 보안 설정을 활성화하는데 사용되는 어노테이션입니다. 이 어노테이션을 사용하면 메서드 단위의 접근 제어를 설정할 수 있으며,@Secured @PreAuthorize, @PostAuthorize와 같은 보안 어노테이션을 사용할 수 있게 됩니다.

@EnableGlobalMethodSecurity 어노테이션에는 두 개의 속성이 있습니다:

  1. securedEnabled: @Secured 어노테이션을 활성화할지 여부를 나타냅니다. @Secured 어노테이션은 메서드에 특정 권한을 가진 사용자만 접근할 수 있도록 지정하는데 사용됩니다.

  2. prePostEnabled: @PreAuthorize와 @PostAuthorize 어노테이션을 활성화할지 여부를 나타냅니다. @PreAuthorize 어노테이션은 메서드 실행 전에, @PostAuthorize 어노테이션은 메서드 실행 후에 특정 조건을 확인하여 접근을 허용하거나 거부하는데 사용됩니다.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    // 추가적인 설정이 필요한 경우 여기에 작성할 수 있습니다.
}
@Service
public class MyService {
    @Secured("ROLE_ADMIN")
    public void doAdminTask() {
        // 관리자 권한이 있는 사용자만 이 메서드에 접근할 수 있습니다.
    }

    @PreAuthorize("hasRole('ROLE_USER')")
    public void doUserTask() {
        // 사용자 권한이 있는 사용자만 이 메서드에 접근할 수 있습니다.
    }

    @PostAuthorize("returnObject.userId == authentication.principal.userId")
    public MyData getUserData(int userId) {
        // 메서드 실행 후에 해당 데이터의 userId가 현재 인증된 사용자의 userId와 일치하는지 확인합니다.
        // 일치하는 경우에만 데이터를 반환합니다.
    }
}
profile
발전하기 위한 공부

0개의 댓글