Spring Security를 이용한 로그인/로그아웃

Soomin Kim·2022년 3월 17일
0

Spring Boot Project

목록 보기
1/2

기술스택

  • Spring Boot
  • Java 11
  • Gradle
  • MyBatis, MySQL
  • IDE: IntellJ IDEA
  1. 프로젝트 생성
    File -> New -> Project -> Spring Initializr

Dependencies 추가

프로젝트 생성 완료

설정
IntellJ IDEA -> Preferences
Build, Execution, Deployment 메뉴 -> Compiler -> Annotation Processors
에서 Enable annotatino processing 체크 후 저장

JDK 설치(by homebrew)
1. Homebrew 설치 및 업데이트

brew update
  1. adoptopenjdk/oepnjdk 추가하기
brew tap adoptopenjdk/openjdk
  1. 설치 가능한 모든 jdk 찾기
brew search jdk
  1. 원하는 버전 설치
    ex) 11버전
brew install --cask adoptopenjdk11
  1. 자바 설치된 곳 확인
/usr/libexec/java_home -V
  1. 자바 버전 확인
java --version

mysql 설치

  1. 설치
brew install mysql
  1. mysql 서비스 시작
brew services start mysql
  1. 로그인해서 접속
mysql -uroot
  1. 비밀번호 등 환경설정 세팅
mysql_secure_installation
  1. 콘솔 명령어 종료
exit
  1. mysql 서비스 종료
brew services stop mysql

DB 생성

mysql -uroot -p
use mysql
CREATE DATABASE {데이터베이스이름};

사용자 생성

CREATE USER '{유저네임}'@'localhost' IDENTIFIED BY '{비밀번호}';

어떤 클라이언트에서든 접근 가능하게 설정

CREATE USER '{username}'@'%' IDENTIFIED BY '{password}';

권한부여

GRANT ALL PRIVILEGES ON {database}.* TO '{username}'@'localhost';

권한 부여한 것을 DBMS에 적용

FLUSH PRIVILEGES;

데이터베이스 접속

mysql -h127.0.0.1 -u{username} -p {database}

로그인을 위한 테이블 생성

User Table

  • 기본적인 인증에 필요한 속성
  • Spring Security에서 제공하는 속성들
CREATE TABLE User(
`id` varchar(15),
`password` varchar(500),
`isAccountNonExpired` tinyint(1),
`isAccountNonLocked` tinyint(1),
`isCredentialsNonExpired` tinyint(1),
`isEnabled`tinyint(1)
) ENGINe=InnoDB DEFAULT CHARSET=utf8;

Authority Table

  • 각 유저별로 할당되는 권한
CREATE TABLE Authority(
`username` varchar(20),
`authority_name` varchar(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

의존성 설정

<build.gradle>

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

    implementation 'mysql:mysql-connector-java:5.1.47'

    // mybatis
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1'
}

mybatis - Spring Boot 연동

mybatis란?

mybatis에서 spring-boot-starter와 연동하도록 지원해주는 모듈 있음

build.gradle에 의존성 추가

implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.2")
implementation('org.springframework.boot:spring-boot-starter-jdbc')
implementation('mysql:mysql-connector-java')

mysql에 접속하기 위한 설정

  • application.properties지우고 application.yml 추가
spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/DB이름?autoReconnect=true&useSSL=false
    username: username(특별히 정해준적 없다면 root)
    password: userpassword

Mapper 생성

  • 자신의 Table Schema와 일치하는 형식으로 User Class 생성하고 해당 객체를 반환하는 Mapper Class 생성

Controller가 사용자가 유효한지 검증하고 그다음에 유저객체 만들어서 Repository를 통해 저장.
그 다음 사용자가 로그인하려고할때 영속화된 데이터를 읽어와서 순간 Spring Security가 유저정보를 읽어와서(UserDetail이라는 객체를 통해) 패스워드비교해서 맞으면 authority 주고 원하는 페이지로 이동시켜줌

우리가 쓰는 걸 userDetail로 바꿔줘야 됨

Spring-security 의존성 추가

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

의존성 적용
View-Tool Windows - Project 누르고
해당 프로젝트 우클릭 - Reload Gradle Project

User Table과 매핑되는 클래스와 매퍼 작성

주의: User라는 이름으로 제공되는 클래스가 이미 있으니, 다른 이름을 사용할 것! 보통 Account를 쓴다고 함

Account.java

package com.example.project1.account;

import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Data;


public class Account implements UserDetails{
    private String id;
    private String password;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;
    private Collection <? extends GrantedAuthority> authorities;

    public String getId() {
        return id;
    }
    public void setId(String id){
        this.id=id;
    }

    @Override
    public Collection <? extends GrantedAuthority> getAuthorities() {
        // TODO Auto-generated method stub
        return this.authorities;
    }
    @Override
    public String getPassword() {
        // TODO Auto-generated method stub
        return this.password;
    }

    public void setPassword(String password){
        this.password = password;
    }
    public void setAccountNonExpired(boolean isAccountNonExpired){
        this.isAccountNonExpired = isAccountNonExpired;
    }
    public void setAccountNonLocked(boolean isAccountNonLocked){
        this.isAccountNonLocked = isAccountNonLocked;
    }
    public void setCredentialsNonExpired(boolean isCredentialsNonExpired){
        this.isCredentialsNonExpired = isCredentialsNonExpired;
    }
    public void setEnabled(boolean isEnabled){
        this.isEnabled = isEnabled;
    }

    public void setAuthorities(Collection <? extends GrantedAuthority> authorities){
        this.authorities = authorities;
    }
    @Override
    public String getUsername() {
        // TODO Auto-generated method stub
        return this.id;
    }
    @Override
    public boolean isAccountNonExpired() {
        // TODO Auto-generated method stub
        return this.isAccountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        // TODO Auto-generated method stub
        return this.isAccountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        // TODO Auto-generated method stub
        return this.isCredentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        // TODO Auto-generated method stub
        return this.isEnabled;
    }

}

아까 DB에서 정의한 User Table이 이 Account Class에서 extend하고 있는 UserDetails interface를 기반으로 한 것!

AccountMapper.java

package com.example.project1.account;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface AccountMapper {
    @Select("SELECT * FROM USER WHERE id=#{id}")
    Account readAccount(String id);

    @Select("SELECT authority_name FROM AUTHORITY WHERE username=#{id}")
    List readAuthorities(String id);

    @Insert("INSERT INTO USER VALUES(#{account.id},#{account.password},#{account.isAccountNonExpired},#{account.isAccountNonLocked},#{account.isCredentialsNonExpired},#{account.isEnabled})")
    void insertUser(@Param("account") Account account);

    @Insert("INSERT INTO AUTHORITY VALUES(#{id},#{authority})")
    void insertUserAuthority(@Param("id") String id, @Param("authority") String authority);

    @Select("SELECT* FROM USER")
    List readAllUsers();
}

실제로 db에 값을 넣어 줄 매퍼

AccountService.java

package com.example.project1.account;

import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import com.example.project1.account.Account;

import org.hibernate.annotations.common.util.impl.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AccountService implements UserDetailsService {
    @Autowired
    AccountRepository accounts;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        Account account = accounts.findById(username);
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        account.setAuthorities(getAuthorities(username));

        UserDetails userDetails = new UserDetails() {

            @Override
            public boolean isEnabled() {
                // TODO Auto-generated method stub
                return true;
            }

            @Override
            public boolean isCredentialsNonExpired() {
                // TODO Auto-generated method stub
                return true;
            }

            @Override
            public boolean isAccountNonLocked() {
                // TODO Auto-generated method stub
                return true;
            }

            @Override
            public boolean isAccountNonExpired() {
                // TODO Auto-generated method stub
                return true;
            }

            @Override
            public String getUsername() {
                // TODO Auto-generated method stub
                return account.getId();
            }

            @Override
            public String getPassword() {
                // TODO Auto-generated method stub
                return account.getPassword();
            }

            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                // TODO Auto-generated method stub

                return account.getAuthorities();
            }
        };

        return account;
    }

    public Account save(Account account, String role) {
        // TODO Auto-generated method stub

        account.setPassword(passwordEncoder.encode(account.getPassword()));
        account.setAccountNonExpired(true);
        account.setAccountNonLocked(true);
        account.setCredentialsNonExpired(true);
        account.setEnabled(true);
        return accounts.save(account, role);
    }


    public Collection<GrantedAuthority> getAuthorities(String username) {
        List<String> string_authorities = accounts.findAuthoritiesById(username);
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        for (String authority : string_authorities) {
            authorities.add(new SimpleGrantedAuthority(authority));
        }
        return authorities;
    }
}

save는 회원가입 대신 유저를 추가해줄 임의의 메서드.
PasswordEncoder은 Spring Security가 우리 대신 패스워드를 암호화해주는 인터페이스

SecurityConfig.java

package com.example.project1;

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.WebSecurityConfigurerAdapter;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // configure 메서드는 어떤 URL path를 secure할지 말지 설정
        http
                .authorizeRequests()
                .antMatchers("/", "/login","/home","/create").permitAll() // home은 아무나 허용
                .anyRequest().authenticated() //
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}

authorizeRequest()는 각 경로에 따른 권한 지정
/resources/**는 css, js 등 뷰 구현고 관련된 파일 권한

hasRole은 괄호 안의 권한을 가진 유저만 해당 경로에 접근할 수 있도록 설정
이때 자동으로 앞에 "ROLE"이 삽입되므로, Authority Table에 사용자의 권한을 삽입할 때 "ROLE권한명"형식으로 삽입해야 됨.

formLogin() 아래는 .loginPage(), .defaultSuccessPage() 등으로 직접 구현한 폼 로그인 성공 시 이동할 경로 지정 가능

AccountRepository.java

package com.example.project1.account;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;


@Repository
public class AccountRepository {
    @Autowired
    AccountMapper accountMapper;


    public Account save(Account account, String role) {
        accountMapper.insertUser(account);
        accountMapper.insertUserAuthority(account.getId(), role);
        return account;
    }

    public Account findById(String username) {
        // TODO Auto-generated method stub
        return accountMapper.readAccount(username);
    }

    public List<String> findAuthoritiesById(String username) {
        return accountMapper.readAuthorities(username);
    }
}

사용자 계정과 권한을 인자로 받아 DB에 삽입.

AccountController.java
사용자 추가할 경로 추가

package com.example.project1.account;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {
    @Autowired
    AccountService accountService;

    @Autowired
    AccountMapper accountMapper;


    //ADMIN 계정 부여
    @GetMapping("/create")
    public Account create(){
        Account account = new Account();
        account.setId("admin");
        account.setPassword("1234");
        accountService.save(account, "ROLE_ADMIN");
        return account;
    }

    //서비스 권한 부여

}

localhost:8080/create로 접속하면 사용자 추가 되고 암호화된 패스워드가 삽입되는 걸 확인할 수 있다!

추가된 사용자로 로그인하면 로그인 되는 걸 확인할 수 있다!!!

[참고문헌]
https://spring.io/guides/gs/securing-web/
https://a1010100z.tistory.com/14?category=791304
https://spring.io/guides/gs/securing-web/

profile
개발자지망생

0개의 댓글