스프링부트 - 스프링시큐리티 세션방식

스프링부트에서 Redis를 연동하는 연습용 프로젝트를 만들어 보겠습니다!

스프링시큐리티 기본 개념

깃헙에 있는 정리본을 참고해주세요!

https://github.com/namusik/TIL-SampleProject/blob/main/Spring%20Boot/%EC%8A%A4%ED%94%84%EB%A7%81%20%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%20%EA%B0%9C%EB%85%90.md

소스코드

https://github.com/namusik/TIL-SampleProject/tree/main/Spring%20Boot/%EC%8A%A4%ED%94%84%EB%A7%81%20%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%20%EC%84%B8%EC%85%98%EB%B0%A9%EC%8B%9D

작업환경

IntelliJ
Spring Boot
java 11
gradle

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:2.6.3'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

스프링시큐리티, h2 database, 타임리프 의존성을 추가해줬습니다.

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:springminiprojectdb
spring.datasource.username=sa
spring.datasource.password=

회원 정보를 저장할 DB로 간단한 h2를 사용해줍니다.
(대신 서버가 종료되면 DB도 날라감!)

WebSecurityConfig

스프링 시큐리티 의존성을 추가하면, WebSecurityConfigurerAdapter 클래스가 실행됨.

여기서는 시큐리티의 초기화 및 설정들을 담당하고 있음.

개인 프로젝트에 맞춰서 WebSecurityConfigurerAdapter를 상속한 커스텀 Config를 만들어주면 됨.

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

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

    //스프링시큐리티 앞단 설정 해주는 곳.
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    //스프링시큐리티의 설정을 해주는 곳
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //URL 인증여부 설정.
        http.authorizeRequests()
                .antMatchers("/images/**", "/user/signup").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated();

        //로그인 관련 설정.
        http.formLogin()
                .loginPage("/user/login")
                .loginProcessingUrl("/user/login")
                .defaultSuccessUrl("/")
                .failureUrl("/user/login?error")
                .and()
                .logout()
                .logoutUrl("/user/logout")
                .logoutSuccessUrl("/")
                .permitAll();
    }
}

Annotation

@EnableWebSecurity

Security 활성화 Annotation. 

클릭해보면 @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class,HttpSecurityConfiguration.class })이 달려서 
해당 Class들을 실행시켜줌.

또한, @AuthenticationPrincipal을 통해 Authentication 객체 속에 있는 principal 필드 가져올 수 있음.

@EnableGlobalMethodSecurity(securedEnabled=true)

Controller에 직접 @Secured("ROLE_ADMIN")을 쓸 수 있게 됨.

여기서 잠깐! HttpSecurity permitAll()과 WebSecurity ingnoring() 차이는??

WebSecurity는 Spring Security Filter Chain을 아예 거치지 않기에

"인증", "인가" 모두 적용되지 않음. 또한, XSS에 대한 보호가 제공안됨. 

HttpSecurity보다 우선 적용되기 때문에 두곳에 모두 설정해도 WebSecurity만 적용됨.


HttpSecurity는 "인증"을 무시함. Security Filter Chain을 거쳐서 요청이 들어옴.

HttpSecurtiy

!!중요한 점은, HttpSecurity 객체를 통해 스프링시큐리티의 각종 설정을 해준다는 점!!

URL 접근권한 설정

http.authorizeRequests()
	.antMatchers("/login", "/web-resources/**")
    뒤에 써준 URL(리소스)에 대해 권한을 설정
    
    .antMatchers("/login", "/web-resources/**").permitAll()
    해당 URL에는 인증절차(로그인) 없이 접근허용
    
    .antMathcers("/admin/**").hasAnyRole("ADMIN")
	해당 URL에는 ADMIN레벨 권한을 가진 사용자만 접근허용
    
    .anyRequest().authenticated();
    나머지 URL은 모두 스프링시큐리티 인증을 거쳐야됨을 설정.

로그인 관련 설정

http.formLogin()
일반적인 로그인 방식, 로그인 Form페이지와 로그인 성공/실패 처리 등을 사용하겠다는 의미.
	
    .loginPage("/user/login") Get방식
    로그인 페이지로 넘어가는 Get 방식 Controller URI.
    따로 설정해주지않으면 Default값인 "/login"로 설정되어있는 기본로그인페이지 제공됨. 
    
    .loginProcessUrl("/user/login") Post방식.
    해당 Controller URI로 요청을 할 경우 자동으로 스프링시큐리티 로그인 인증과정이 시작되도록 설정.
    
    .defaultSuccessUrl("/") Get방식 
    로그인 성공시 요청하는 Controller URI
    설정하지 않는경우 "/"값이 defaultf로 설정됨.
    
    .successHandler(new AuthenticationSuccessHandlerImpl("/main"))
    로그인 성공했을 때 추가 로직을 해주기 위한 커스텀 핸들러. 
    AuthenticationSuccessHanlder의 구현체.
    
    .failureUrl("/user/login?error")
    로그인 실패했을 때 요청하는 Controller URI
    
    .failureHandler(new AuthenticationFailureHandlerImpl("/login/fail)
    로그인 실패했을 때 추가 로직을 위한 커스텀 핸들러 설정.

RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(ChatMessage.class));
        return redisTemplate;
    }
}

Json 형식으로 객체를 Value에 저장시키기 때문에 valueSerializer를 Jackson으로 미리 설정해놓은 RedisTemplate을 만들어 준다.

StringRedisTemplate은 따로 Bean으로 안만들어도 쓸 수 있음.

RedisService

	@Service
    @RequiredArgsConstructor
    public class RedisService {

        private final StringRedisTemplate stringRedisTemplate;

        public void setRedisStringValue(ChatMessage chatMessage) {
            ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
            stringValueOperations.set("sender", chatMessage.getSender());
            stringValueOperations.set("context", chatMessage.getContext());
        }

        public void getRedisStringValue(String key) {

            ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
            System.out.println(key +" : " + stringValueOperations.get(key));
        }
    }
    
        public void setRedisValue(ChatMessage chatMessage) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        String key = chatMessage.getSender();
        valueOperations.set(key, chatMessage);
    }

        public void getRedisValue(String key) {
            ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
            ChatMessage chatMessage = (ChatMessage) valueOperations.get(key);
            System.out.println("sender = " + chatMessage.getSender());
            System.out.println("context = " + chatMessage.getContext());
        }

Redis 서비스클래스

RedisTemplate<String, Object>는 get/set을 위한 객체.

template에서 valueOperation객체를 받아 사용함.

스프링부트에서는 아래의 redisTemplate이 자동생성되는데,

@Autowired RedisTemplate redisTemplate; 
@Autowired StringRedisTemplate stringRedisTemplate; 
@Autowired ReactiveRedisTemplate reactiveRedisTemplate; 
@Autowired ReactiveStringRedisTemplate reactiveStringRedisTemplate;

redisTemplate 과 stringRedsiTemplate는 직렬화에 차이가 있음

stringRedisTemplate은 문자열에 특화된 template제공. 대부분 레디스 key-value는 문자열 위주기 때문에.

redisTemplate은 자바 객체를 redis에 저장하려고 할 때 사용하면 됨.

setRedisStringValue(ChatMessage chatMessage)를 통해 전달받은 메시지객체를 각각 redis에 set해줌.

getRedisStringValue(Strin key)통해 제대로 저장이 되었는지 출력해봄.

setRedisValue(ChatMessage chatmessage) 는 redisTemplate을 사용해서 value에 자바 객체 자체를 저장시킴

RedisController

@RestController
@RequiredArgsConstructor
public class RedisController {

    public final RedisService redisService;

    @PostMapping("api/redisStringTest")
    public String send(@RequestBody ChatMessage chatMessage) {
        redisService.setRedisStringValue(chatMessage);

        redisService.getRedisStringValue("sender");
        redisService.getRedisStringValue("context");

        return "success";
    }
    
    @PostMapping("api/redisTest")
    public String send(@RequestBody ChatMessage chatMessage) {
        redisService.setRedisValue(chatMessage);

        String key = chatMessage.getSender();
        redisService.getRedisValue(key);

        return "success";
    }
}

RedisService를 생성자 의존성주입으로 가져와서 JSON형태로 객체를 전달받으면 set/get을 한번에 실행시켜서 최종적으로 출력시켜보는 Controller.

두 방식 모두 같은 결과로 나옴

실행결과

Postman을 사용해서 api를 실행시키면

성공시 "success"를 반환하고

저장해준 ChatMessage의 sender와 context가 제대로 출력되면 성공

참고

https://kimchanjung.github.io/programming/2020/07/03/spring-security-03/

https://velog.io/@seongwon97/security

https://bcp0109.tistory.com/303

좋은 웹페이지 즐겨찾기