본문 바로가기

개발(합니다)/Java&Spring

[spring boot 설정하기-8] security 설정 및 테스트 소스

반응형

인증은 어디서나 중요한 부분인데 spring boot에서는 기본적으로 제공해주는 security가 있습니다.

설정하는 방법과 사용하는 방법이 방대하여 기본적인 설정방법만 작성합니다.

관련 정보는 아래 사이트에서 확인할 수 있습니다.

docs.spring.io/spring-security/site/docs/5.0.19.RELEASE/reference/htmlsingle/

spring.io/guides/gs/securing-web/


1. 의존성 추가

로그인 화면을 만들기 위해 "thymeleaf"도 포함합니다.

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    implementation 'org.springframework.security:spring-security-test'

2. security config 설정 및 소스

패키지 구성은 아래 사진과 같습니다.
db.changelog-2.0.xml도 추가 된것을 확인할 수 있습니다.

2-1. MvcConfig.java

화면과 연결하기 위한 web에 대한 기본적인 설정을 해줍니다.

package com.otrodevym.spring.base.common.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class MvcConfig implements WebMvcConfigurer {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/public/", "classpath:/",
            "classpath:/resources/", "classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/" };

    public void addViewControllers(ViewControllerRegistry registry) {
        // 경로에 해당하는 url을 forword 합니다.
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/secu/user/login").setViewName("login");


        // 우선순위를 가장 높게 잡는다.
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 자원의 경로를 허용합니다.
        registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
    }
}

2-2. WebSecurityConfig.java

security에 대한 설정을 합니다.

package com.otrodevym.spring.base.common.security.config;

import com.otrodevym.spring.base.common.security.SecurityMemberService;
import com.querydsl.core.annotations.Config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
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.builders.WebSecurity;
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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;


@Config
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

// 비밀번호 암호화를 위한 서비스 호출
    private SecurityMemberService securityMemverService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
    // 암호화
        return new BCryptPasswordEncoder();
    }

    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    // WebSecurity는 FilterChainProxy 생성 필터입니다.
    @Override
    public void configure(WebSecurity web) {
        // 해당 경로의 파일들은 spring security가 무시하도록합니다.
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
//                .csrf().disable() // csrf를 사용할지 여부
                .authorizeRequests() // HttpServletRequest에 따라접근을 제한
                .antMatchers("/admin/**").hasRole("ADMIN") // url에 따른 권한만 허용
                .antMatchers("/info").hasRole("MEMBER") // url에 따른 권한만 허용
                .antMatchers("/**").permitAll() // url에 따른 모두 허용
            .and()
                .formLogin() // form 기반 로그인 인증 관련하며 HttpSession 이용
                .loginPage("/login") // 지정하고 싶은 로그인 페이지 url 
                .usernameParameter("email") // 지정하고 싶은 username name 명칭이며, 기본은 username
                .passwordParameter("upwd") // 지정하고 싶은 password name 명칭이며, 기본은 password
                .defaultSuccessUrl("/login/result") // 로그인 성공 시 이동페이지
                .permitAll() // 모두 접근 허용
            .and()
                .csrf() // csrf 사용
                .ignoringAntMatchers("/h2-console/**") // csrf 제외 url 
                .ignoringAntMatchers("/post/**") // csrf 제외 url
                .ignoringAntMatchers("/admin/**") // csrf 제외 url
            .and()
                .logout() // 로그아웃 
                .logoutRequestMatcher(new AntPathRequestMatcher(("/logout"))) // 지정하고 싶은 로그아웃 url
                .logoutSuccessUrl("/index") // 성공 시 이동 페이지
                .invalidateHttpSession(true) // 세션 초기화
//              .deleteCookies("cookies") // 특정 쿠키를 제거
            .and()
                .exceptionHandling() // 에러 처리
                .accessDeniedPage("/error"); // 에러 시 이동할 페이지
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 비밀번호 암호화
        auth.userDetailsService(securityMemverService).passwordEncoder(bCryptPasswordEncoder());
    }
}

2-3. application.yml 추가 설정

thymeleaf에 대한 설정을 합니다.

  thymeleaf:
    check-template-location: true
    suffix: .html
    mode: HTML5
    characterEncoding: utf-8
    cache: false
    order: 0
    prefix: classpath:/templates/

3. spring security 테스트 소스

테스트 하기 위한 예제입니다.

3-1. SecurityController.java

package com.otrodevym.spring.base.common.security;

import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
@AllArgsConstructor
@Log4j2
public class SecurityController {
    private SecurityMemberService securityMemberService;

    // 메인 페이지
    @GetMapping("/index")
    public String index() {        log.info("index");        return "index";    }

    // 회원 가입 페이지
    @GetMapping("/signup")
    public String dispSignup() {        log.info("dispSignup : /signup");        return "signup";    }

    // 회원가입 처리
    @PostMapping("/signup")
    public String execSignup(SecurityMemberDTO securityMemberDTO) {        log.info("execSignup : /signup");
        securityMemberService.siginupMember(securityMemberDTO);        return "redirect:/secu/user/login";    }

    // 로그인 페이지
    @GetMapping("/login")
    public String dispLogin() {        log.info("dispLogin");        return "login";    }

    // 로그인 결과 페이지
    @GetMapping("/login/result")
    public String dispLoginResult() {        log.info("dispLoginResult");        return "login_success";    }

    // 로그아웃 페이지
    @GetMapping("/logout/result")
    public String dispLogoutResult() {        log.info("dispLogoutResult");        return "logout";    }

    @GetMapping("/info")
    public String dispInfo() {        log.info("dispInfo");        return "info";    }

    @GetMapping("/admin")
    public String dispAdmin() {        log.info("dispAdmin");        return "admin";    }
}

 

3-2. SecurityMemberService.java

package com.otrodevym.spring.base.common.security;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;



@Service
@AllArgsConstructor
@Slf4j
public class SecurityMemberService implements UserDetailsService {

    private SecurityRepository securityRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SecurityMemberEntiry> securityMemberEntiryOptional = securityRepository.findByEmail(username);
        SecurityMemberEntiry securityMemberEntiry = securityMemberEntiryOptional.orElse(null);


        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(securityMemberEntiry.getRoleKey()));


//        return new SecurityCustomDTO(securityMemberEntiry, securityMemberEntiry.getName(),
//                securityMemberEntiry.getUpwd(), authorities);

        if ("admin@test.com".equals(username)) {
            authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getKey()));
        } else {
            authorities.add(new SimpleGrantedAuthority(Role.MEMBER.getKey()));
        }
        log.info("username : " + username);
        return new User(securityMemberEntiry.getEmail(), securityMemberEntiry.getUpwd(), authorities);
    }

    public SecurityMemberEntiry siginupMember(SecurityMemberDTO securityMemberVO) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        securityMemberVO.setUpwd(bCryptPasswordEncoder.encode(securityMemberVO.getUpwd()));

        return securityRepository.save(securityMemberVO.toEntity());
    }
}

3-3. SecurityMemberEntiry.java

package com.otrodevym.spring.base.common.security;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;


@Getter
@Entity(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SecurityMemberEntiry extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, unique = true, nullable = false)
    private String email;

    @Column(unique = true, nullable = false)
    private String name;

    @Column(unique = true, length = 300)
    private String upwd;

    @Enumerated(EnumType.STRING)
    @Column
    private Role role;


    @Builder
    public SecurityMemberEntiry(Long id, String email, String name, String upwd, Role role) {
        this.id = id;
        this.email = email;
        this.name = name;
        this.upwd = upwd;
        this.role = role;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }

    public SecurityMemberEntiry update(String name, String email) {
        this.name = name;
        this.email = email;
        return this;
    }
}

3-4. SecurityMemberDTO.java

package com.otrodevym.spring.base.common.security;

import lombok.*;

import java.time.LocalDateTime;


@Getter
@Setter
@ToString
@NoArgsConstructor
public class SecurityMemberDTO {

    private Long id;
    private String name;
    private String email;
    private String upwd;
    private Role role;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;

    public SecurityMemberEntiry toEntity() {
        return SecurityMemberEntiry.builder()
                .id(this.id)
                .name(this.name)
                .email(this.email)
                .upwd(this.upwd)
                .role(Role.MEMBER)
                .build();
    }

    @Builder
    public SecurityMemberDTO(Long id, String name, String email, String upwd, Role role, LocalDateTime createdDate, LocalDateTime modifiedDate) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.upwd = upwd;
        this.role = role;
    }
}

3-5. BaseTimeEntity.java

package com.otrodevym.spring.base.common.security;

import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;


@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @UpdateTimestamp
    @Column()
    private LocalDateTime modifiedDate;

}

3-6. SecurityRepository.java

package com.otrodevym.spring.base.common.security;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SecurityRepository extends JpaRepository<SecurityMemberEntiry, Long> {

    Optional<SecurityMemberEntiry> findByEmail(String username);
}

3-9. Role.java

package com.otrodevym.spring.base.common.security;

import lombok.AllArgsConstructor;
import lombok.Getter;


@AllArgsConstructor
@Getter
public enum Role {
    ADMIN("ROLE_ADMIN", "어드민"),
    MEMBER("ROLE_MEMBER", "사용자");

    private String key;
    private String title;
}

3-10. admin.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>admin</title>
</head>
<body>
  <h1>어드민 페이지</h1>
  <hr>
</body>
</html>

3-11. error.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
  <h1>에러 페이지</h1>
</body>
</html>

3-12. index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8">
    <title>Main</title>
</head>
<body>
<h1>메인 페이지</h1>

<hr>

<a sec:authorize="isAnonymous()" th:href="@{/login}">로그인</a>
<a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
<a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
<a sec:authorize="hasRole('ROLE_MEMBER')" th:href="@{/info}">내정보</a>
<a sec:authorize="hasRole('ROLE_ADMIN')" th:href="@{/admin}">어드민</a>


</body>
</html>

3-13. info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>내 정보</title>
</head>
<body>
<h1>내 정보 페이지</h1>
</body>
</html>

3-14. login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form th:action="@{/login}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

    <input type="text" name="email" placeholder="이메일 입력해주세요">
    <input type="password" name="upwd" placeholder="비밀번호">
    <button type="submit">로그인</button>
</form>

</body>
</html>

3-15. login_success.html

<!DOCTYPE html>
<html lang="en" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>로그인 성공</title>
</head>
<body>
<h1>로그인 성공!</h1>
<p>
    <span sec:authentication="name"></span> 님 안녕하세요
</p>
<a th:href="@{'/index'}">메인 페이지</a>
</body>
</html>

3-16. logout.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그아웃</title>
</head>
<body>
<h1>로그아웃 완료!</h1>
<hr>
<a th:href="@{'/index'}">메인 페이지</a>
</body>
</html>

3-17. signup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>


<form th:action="@{/signup}" method="post">
    <input type="text" name="email" placeholder="이메일 입력해주세요">
    <br>
    <input type="text" name="name" id="name" placeholder="Your ID"/>
    <br>
    <input type="password" name="upwd" placeholder="비밀번호">
    <br>
    <input type="password" name="re_upwd" id="re_pass" placeholder="Repeat your password"/>
    <br>
    <button type="submit">가입하기</button>
</form>


</body>
</html>

3-18. db.changelog-2.0.xml

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

    <changeSet author="otrodevym" id="changelog-2.0">
        <createTable tableName="member">
            <column name="id" type="bigint" autoIncrement="true">
                <constraints primaryKey="true"/>
            </column>
            <column name="email" type="varchar(30)"></column>
            <column name="name" type="varchar(30)"></column>
            <column name="role" type="varchar(30)"></column>
            <column name="upwd" type="varchar(300)"></column>
            <column name="modified_date" type="timestamp" defaultValueDate="now()">
                <constraints nullable="false"/>
            </column>
            <column name="created_date" type="timestamp" defaultValueDate="now()">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </changeSet>

</databaseChangeLog> 

4. 실행 결과

 

index로 접속합니다

 

메인 페이지와 회원가입

로그인 화면, 로그인 성공 결과, 메인페이지 변화, 내정보 페이지

반응형