1. open-jdk 1.8 설치

# yum install java-1.8.0-openjdk
# yum install java-1.8.0-openjdk-devel

설치가 완료되면 /usr/bin/경로에 java가 생성됩니다.


2. 환경변수 등록

/usr/bin/java 경로에 심볼릭링크가 걸려있기 때문에 실제 경로를 찾아서 환경변수에 등록해주어야 합니다.

# readlink -f /usr/bin/java
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/jre/bin/java

실제 경로를 찾았으면 /etc/profile을 vi로 열어줍니다. 그리고 JAVA_HOME, PATH, CLASSPATH를 등록합니다.

//# vi /etc/profile

...

JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar

export JAVA_HOME PATH CLASSPATH

환경 변수를 등록했다면 ssh연결을 재시작하거나 source /etc/profile 명렁어를 입력해줍니다.

등록한 환경 변수가 제대로 적용되었는지 테스트합니다.

# echo $JAVA_HOME
# echo $PATH
# echo $CLASSPATH

3. HelloWorld.java 컴파일 후 실행

# vi HelloWorld.java
public class HelloWorld{
   public static void main(String[] args){
        System.out.println("Hello World!!");
   }
}

HelloWorld.java 파일을 컴파일하고 실행시켜서 테스트해봅니다.

# javac HelloWorld.java
# java -cp . HelloWorld
Hello World!!

 

테스트 끝!


도움이 되셨다면 공감버튼을 눌러주세요!

CentOS 7에 Tomcat8을 설치 및 설정하는 예제를 진행하겠습니다. 테스트의 편의성을 위해 root 계정에서 진행하겠습니다.

1. open-jdk 1.8 설치 

jdk가 설치되어 있지 않다면 아래 링크에서 다운로드해주세요.

2020/02/19 - [IT/Linux] - [Linux] CentOS 7에 OpenJDK 1.8 설치

2. tomcat 설치

tomcat은 설치하는 방법이 다양합니다. 이번 예제는 wget을 이용해 설치하겠습니다.

# wget http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.27/bin/apache-tomcat-8.5.27.tar.gz

// 압축 해체
# tar zxvf apache-tomcat-8.5.27.tar.gz

// 톰캣을 /usr/local/로 이동시키고 디렉토리 이름을 tomcat8로 변경
# mv apache-tomcat-8.5.27 /usr/local/tomcat8

3. tomcat 설정 및 환경 변수 등록

vi /usr/local/tomcat8/conf/server.xml
// vi /usr/local/tomcat8/conf/server.xml
// 아래 설정을 찾아서 URIEncoding="UTF-8"을 추가한다.

...
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               URIEncoding="UTF-8" />
...
vi /etc/profile
...

JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64
CATALINA_HOME=/usr/local/tomcat8
CLASSPATH=$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar:$CATALINA_HOME/lib-jsp-api.jar:$CATALINA_HOME/lib/servlet-api.jar
PATH=$PATH:$JAVA_HOME/bin:/bin:/sbin
export JAVA_HOME PATH CLASSPATH CATALINA_HOME
설정 후 아래 명령어 입력 
# source /etc/profile

3. tomcat 실행

startup.sh파일은 tomcat을 실행시키는 shell script입니다. 해당 파일을 실행시켜 tomcat을 작동시켜줍니다.

# /usr/local/tomcat8/bin/startup.sh

tomcat이 실행되고 있는지 확인합니다.

//톰캣 프로세스 확인
# ps -ef|grep tomcat8

// 8080 포트가 열려있는지 확인 
# netstat -tln

 테스트해보기 위해 다음 명령어를 실행시켜줍니다.

# wget http://localhost:8080/

index.html 파일이 다운로드 되었다면 정상적으로 tomcat이 실행되고 있다는 의미입니다. 

index.html 파일은 기본적으로 실행되는 /usr/local/tomcat8/webapps/ROOT WebService에서 다운받은 것이며, 

http://localhost:8080 호출 시,  /usr/local/tomcat8/webapps/ROOT/index.jsp 파일이 index.html로 변환되어 다운됩니다.


4. systemctl 등록

vi /etc/systemd/system/tomcat8.service
# Systemd unit file for tomcat
[Unit]
Description=Apache Tomcat Web Application Container
After=syslog.target network.target

[Service]
Type=forking

Environment="JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/"
Environment="CATALINA_HOME=/usr/local/tomcat8"
Environment="CATALINA_BASE=/usr/local/tomcat8"
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC"
Environment="JAVA_OPTS=-Djava.security.egd=file:///dev/urandom"

ExecStart=/usr/local/tomcat8/bin/startup.sh
ExecStop=/usr/local/tomcat8/bin/shutdown.sh

User=root
Group=root
UMask=0007
RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target

 

# systemctl daemon-reload
# systemctl enable tomcat8

// tomcat8 실행
# systemctl start tomcat8

5. systemctl service 부팅 시 자동 실행

//부팅 시 자동 실행 서비스 등록
# systemctl enable tomcat8.service
//등록된 서비스 조회
# systemctl list-unit-files --type service |grep tomcat8

 

6. tomcat manager 설정 (해당 설정은 선택사항 입니다.)

tomcat manager란 tomcat 모니터링 및 어플리케이션 관리, WAR 배포 및 다양한 서비스를 제공해주는 관리 페이지입니다.

1) tomcat-users.xml 수정

vi /usr/local/tomcat8/conf/tomcat-users.xml
<tomcat-users xmlns="http://tomcat.apache.org/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd" version="1.0">

  <role rolename="manager"/>
  <role rolename="manager-gui" />
  <role rolename="manager-script" />
  <role rolename="manager-jmx" />
  <role rolename="manager-status" />
  <role rolename="admin"/>
  <user username="admin" password="패스워드" roles="admin,manager,manager-gui, manager-script, manager-jmx,  manager-status"/>

</tomcat-users>

 

2) 외부 접근 허용 설정

vi /usr/local/tomcat8/conf/Catalina/localhost/manager.xml
<Context privileged="true" antiResourceLocking="false" docBase="${catalina.home}/webapps/manager">          <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="^.*$" />
</Context>

설정 후, 서버 재시작

3) 테스트

http://자신의IP:8080/manager

위 주소를 입력하고 ID와 password를 입력면 아래와 같은 웹페이지가 노출됩니다.  

만약 접근이 안된다면 여러가지 이유가 있겠지만, 8080포트로 방화벽이 열려있는지 확인해주시기 바랍니다.
방화벽에 대한 정보는 아래 링크를 이용해주세요.
2020.03.02 - [IT/Linux] - [Linux] firewalld 방화벽 설정하기

'IT > Linux' 카테고리의 다른 글

[Linux] CentOS7에 MariaDB 설치하기  (8) 2020.02.24
[Linux] CentOS 7에 OpenJDK 1.8 설치  (0) 2020.02.19
[Linux] 계정과 그룹관리  (0) 2020.02.12
[Linux] CentOS 7 Git Server 구축하기  (0) 2020.02.11
[Linux] CentOS 7 Mysql 설치  (0) 2019.12.27

1. 계정 설정

1) 계정 확인하기

# cat /etc/passwd | grep kim
kim:x:1003:1003::/home/kim:/bin/bash

순서대로 사용자명, 패스워드, uid, gid, 설명, 계정 홈 디렉토리, 쉘 환경이다.

 

2) 계정 생성하기

# useradd 계정명 -m -s /bin/bash

 

3) useradd options

-d
홈 디렉토리 경로를 지정해준다.
-m
홈 디렉토리를 생성한다. (경로를 지정해주지 않으면 /home/계정명 으로 생성된다.)
-u
사용자 UID값을 지정한다.
-g
주 그룹으로 소속 시킨다.
-G groups...
보조 그룹을 지정한다. (kim계정이 생성되면 kim 그룹이 생성되고 kim 그룹이 입력한 그룹의 보조그룹이된다.)
-s
셀 경로를 지정한다.
-c
계정 설명을 추가한다.
-e expire_date
계정의 만료일을 설정한다.  ex) -e 2020-12-31
default는 무기한이며, /etc/default/useradd 파일의 EXPIRE 변수의 값에 만료일이 명시되어 있다.

 

4) 계정 비밀번호 변경하기

1) # echo '패스워드' | passwd --stdin 계정명

2) # echo '계정명:패스워드' | chpasswd

3) # passwd 계정명

 

5) 계정 삭제

# userdel -r 계정명

-r : home directory도 함께 삭제된다.


2. 그룹 설정

1) 계정 리스트 검색

# cat /etc/group

 

2) 그룹 생성

# groupadd 그룹명 

 

3) 그룹 삭제

# groupdel 그룹명

 

4) 특정 계정을 특정 그룹의 보조 그룹으로 추가

-g : 주 그룹 변경 옵션

-G : 보조 그룹 추가 옵션

//이미 해당 계정에 보조그룹이 지정되어 있다면, 덮어쓰기 된다.
# usermod -G 그룹명 계정명

//이미 해당 계정에 보조그룹이 있어도 보조그룹이 추가된다.
# usermod -G 그룹명 -a 계정명 

 

5) 특정 계정의 주그룹 변경 

# usermod -g 그룹명 계정명

 

6) 특정 계정이 소속된 그룹 검색

# groups 계정명

 

7) 계정 ID, 그룹 ID, 보조 그룹 검색

# id 계정명

3. 계정 생성 및 그룹 생성 테스트

//4개의 그룹을 생성한다.
# groupadd group1
# groupadd group2
# groupadd group3
# groupadd group4

// user1 계정 생성 시, group1을 주그룹으로 지정하고 group2,group3,group4을 보조그룹으로 지정한다.
# useradd user1 -g group1 -G group2,group3,group4

// "group"으로 시작하는 group을 검색한다.
# cat /etc/group | grep group
group1:x:1008: 
group2:x:1009:user1 
group3:x:1010:user1 
group4:x:1011:user1

//"user1" 계정을 검색한다.
# cat /etc/passwd | grep user1
user1:x:1007:1008::/home/user1:/bin/bash

//"user1"의 id정보들을 검색한다.
# id user1
uid=1007(user1) gid=1008(group1) groups=1008(group1),1009(group2),1010(group3),1011(group4)

 

CentOS 7에 Git을 설치해서 remote repository로 사용하는 예제를 포스팅하겠습니다.

(CentOS 7이 설치되어있지 않다면 다음 링크를 참고해주세요.)

2019/12/24 - [IT/Linux] - [Linux] VMware에 CentOS 7 설치 방법


1. Git 서버

Git 설치

# yum install git

# git --version

git을 설치하고 버전을 확인한다.

Git Directory 및 저장소 생성

mkdir -p /opt/git/project.git

git init --bare /opt/git/project.git/

git directory를 생성한다. (-p 는 상위 디렉토리도 함께 생성하는 옵션)

그리고 해당 디렉토리를 git 환경으로 초기화한다.

Git 계정 생성

# useradd git
# echo 'git:비밀번호입력' | chpasswd
# chown -R git:git /opt/git/

git 계정을 생성하고 비밀번호를 변경한다. 그리고  "/opt/git/" 디렉토리의 소유자를 git으로 변경한다.

(chown에서 -R 옵션은 하위 디렉토리까지 소유자를 변경한다는 의미이다.) 

.ssh 디렉토리 생성 

git 계정 home 디렉토리에 .ssh 디렉토리를 생성한다. 

# cd /home/git/
# mkdir .ssh

2. Windows 클라이언트

git-bash 설치

아래 경로에서 git-bash를 설치한다.

https://git-scm.com/

 

Git

 

git-scm.com

SSH KEY 생성

원격 저장소와 통신하려면, 서버에 공유키(public key)를 등록해야한다. 등록 하기 이전에 먼저 key를 생성해 보자

git-bash를 실행하고 ssh key 저장 디렉토리로 이동한다.

그리고 ssh-keygen명령어를 입력해서 key를 생성한다. (key가 이미 존재한다면 하지않아도 된다.)

무언가를 입력하라는 입력 창이 노출되는데 무시하고 Enter를 누르면 된다.

ssh-key가 정상적으로 생성되었다. ls 명령어를 입력해 잘 생성되었는지 확인한다.

그리고 pwd 명령어로 .ssh 경로를 메모장에 적어둔다. 


SFTP를 이용해 public key를 서버로 이동

filezila 툴을 이용해서 public key(id_rsa.pub)를 서버로 이동시킨다.

filezila가 없다면 아래 경로에서 다운받자.

https://filezilla-project.org/

 

FileZilla - The free FTP solution

Overview Welcome to the homepage of FileZilla®, the free FTP solution. The FileZilla Client not only supports FTP, but also FTP over TLS (FTPS) and SFTP. It is open source software distributed free of charge under the terms of the GNU General Public Licens

filezilla-project.org

 

filezila를 실행시켜 호스트 정보와 사용자 정보를 입력한 후 연결한다.

그리고 미리 생성해두었던 public key를 서버의 "/home/git/.ssh" 디렉토리로 이동시킨다.


서버에 key 등록하기

다시 서버로 돌아와서 /home/git/.ssh 디렉토리 경로로 이동 후 다음 명령어를 입력한다.

cat id_rsa.pub >> authorized_keys

위 명령어를 입력하면 클라이언트의 public key가 authorized_keys 파일에 append 된다.


/opt/git/project.git clone 하기 

프로젝트를 받기위해 적당한 위치에 디렉토리를 미리 생성시켜 둔다.

그리고 git-bash에서 해당 디렉토리로 이동해 다음 명령어를 입력한다.

git clone ssh://git@IP주소:/opt/git/project.git

clone 후 해당 디렉토리로 이동해 .git 폴더를 열어보면 git에 대한 정보가 담긴 파일들이 저장된 것을 볼 수 있다.


원격 저장소에 파일 올리기 

project 디렉토리에 test.txt 파일을 생성한다.

# git add .
# git commit --message "메시지"
# git push origin master


git 사용자가 리눅스 쉘 접근 막기

git을 이용하는데 문제는 없지만, 보안 상 막는게 좋다.

# witch git-shell
# chsh git -s /usr/bin/git-shell
# cat /etc/passwd | grep git

이것을 적용하면 git 계정에 접근할 수 없다.

 

참조

https://zetawiki.com/wiki/%EB%A6%AC%EB%88%85%EC%8A%A4_Git_%EC%84%9C%EB%B2%84_%EA%B5%AC%EC%B6%95_(SSH_%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C)

'IT > Linux' 카테고리의 다른 글

[Linux] CentOS 7에 OpenJDK 1.8 설치  (0) 2020.02.19
[Tomcat] CentOS 7에 Tomcat8 설치 및 설정  (0) 2020.02.19
[Linux] 계정과 그룹관리  (0) 2020.02.12
[Linux] CentOS 7 Mysql 설치  (0) 2019.12.27
[Linux] VMware에 CentOS 7 설치 방법  (0) 2019.12.24

Spring Security 란?

Spring Security는 스프링 기반의 어플리케이션 보안을 담당하는 프레임워크입니다. Spring Security를 사용하면 사용자 인증, 권한, 보안처리를 간단하지만 강력하게 구현 할 수 있습니다.

Spring Boot + Hibernate + SpringSecurity + thymeleaf + mariadb를 이용해 간단한 회원 가입 및 로그인 기능을 구현해보겠습니다.

pom.xml

    ...
    <version>2.1.9.RELEASE</version>
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
<!--        thymeleaf에서 Security 명령어를 사용하기 위해 포함-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
    </dependencies>
    ...

 


application.yml

spring :
    datasource :
        url : jdbc:mariadb://localhost:3306/local_toy
        username : tester
        password : 1234
    jpa :     
        hibernate :
            ddl-auto : update  

Spring Security 설정하기

Spring Security는 FilterChainProxy라는 이름으로 내부에 여러 Filter들이 동작하고 있습니다.

Spring Security는 Filter를 이용해서 기본적인 기능을 간단하게 구현할 수 있습니다. 

설정은 WebSecurityConfigurerAdapter 클래스를 상속받아 오버라이딩하는 방식으로 진행할 수 있습니다.

(Spring Security Filter 및 동작 설명을 알고 싶다면 아래 링크로 이동해주세요.)

2020/02/07 - [IT/Spring] - [Spring Boot] Spring Security의 동작

참조 https://spring.io/guides/topicals/spring-security-architecture#_web_security

클라이언트가 서버에 데이터를 요청하면 DispatcherServlet에 전달되기 이전에 여러 ServletFilter를 거칩니다.

이때 Spring Security에 등록했었던 Filter를 이용해 사용자 보안 관련된 처리를 진행하는데, 연결된 여러개의 Filter들로 구성 되어있어서 FilterChain 이라고 부릅니다.

SpringConfig.java

import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

    @Autowired
    private MemberService memberService;

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

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

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

        http.authorizeRequests()
                .antMatchers("/member/**").authenticated()
                .antMatchers("/admin/**").authenticated()
                .antMatchers("/**").permitAll();

        http.formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll();

        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);

        http.exceptionHandling()
                .accessDeniedPage("/denied");
    }

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

}

Spring Securiry 설정 활성화하기

@Configuration 
해당 클래스를 Configuration으로 등록합니다.
@EnableWebSecurity 
Spring Security를 활성화 시킵니다.
@EnableGlobalMethodSecurity(prePostEnabled = true)
Controller에서 특정 페이지에 특정 권한이 있는 유저만 접근을 허용할 경우 @PreAuthorize 어노테이션을 사용하는데, 해당 어노테이션에 대한 설정을 활성화시키는 어노테이션입니다. (필수는 아닙니다.)

MemberService

로그인 요청 시, 입력된 유저 정보와 DB의 회원정보를 비교해 인증된 사용자인지 체크하는 로직이 정의되어있습니다.

BCryptPasswordEncoder

비밀번호를 복호화/암호화하는 로직이 담긴 객체를 Bean으로 등록합니다.


WebSecurity, HttpSecurity, AuthAuthenticationManagerBuilder configure 설정

오버라이딩configure 메소드들에 대해 설명하겠습니다.

WebSecurity

WebSecurity는 FilterChainProxy를 생성하는 필터입니다. 다양한 Filter 설정을 적용할 수 있습니다.

web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");  

위 설정을 통해 Spring Security에서 해당 요청은 인증 대상에서 제외시킵니다.

HttpSecurity

HttpSecurity를 통해 HTTP 요청에 대한 보안을 설정할 수 있습니다.

http.authorizeRequests()
    .antMatchers("/member/**").authenticated()
    .antMatchers("/admin/**").authenticated()
    .antMatchers("/**").permitAll();

http 요청에 대해서 모든 사용자가 /** 경로로 요청할 수 있지만, /member/** , /admin/** 경로는 인증된 사용자만 요청이 가능합니다. 

authorizeRequests()
HttpServletRequest 요청 URL에 따라 접근 권한을 설정합니다. 
antMatchers("pathPattern")
요청 URL 경로 패턴을 지정합니다.
authenticated()
인증된 유저만 접근을 허용합니다.
permitAll()
모든 유저에게 접근을 허용합니다.
anonymous()
인증되지 않은 유저만 허용합니다.
denyAll()
모든 유저에 대해 접근을 허용하지 않습니다.

로그인 설정을 진행합니다.

http.formLogin()
    .loginPage("/login")
    .defaultSuccessUrl("/")
    .permitAll();

 

formLogin()
form Login 설정을 진행합니다.
loginPage("path")
커스텀 로그인 페이지 경로와 로그인 인증 경로를 등록합니다.
defaultSuccessUrl("path")
로그인 인증을 성공하면 이동하는 페이지를 등록합니다.

로그아웃 설정을 진행합니다.

http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
    .logoutSuccessUrl("/login")
    .invalidateHttpSession(true);
logout()
로그아웃 설정을 진행합니다.
logoutRequestMatcher(new AntPathRequestMatcher("path"))
로그아웃 경로를 지정합니다.
logoutSuccessUrl("/path")
로그아웃 성공 시 이동할 경로를 지정합니다.
invalidateHttpSession(true)
로그아웃 성공 시 세션을 제거합니다.

권한이 없는 사용자가 접근했을 경우 이동할 경로를 지정합니다.

http.exceptionHandling()
    .accessDeniedPage("/denied");

AuthenticationManagerBuilder

AuthenticationManager를 생성합니다. AuthenticationManager는 사용자 인증을 담당합니다.

auth.userDetailsService(service)org.springframework.security.core.userdetails.UserDetailsService 인터페이스를 구현한 Service를 넘겨야합니다. 

auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());

로그인 인증을 위한 회원 정보 체크 로직

비밀번호를 체크하는 로직을 구현하려면 UserDetailsService 인터페이스를 구현한 클래스가 필요합니다.

해당 기능을 구현하기 위해 Member Entity와 Repository, Service 등을 구현해보겠습니다.

Member.java

import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "tb_member")
public class Member {

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

    @Column(length = 255, nullable = false)
    private String name;

    @Column(length = 255, nullable = false, unique = true)
    private String account;

    @Column(length = 255, nullable = false)
    private String password;

    @Column(name = "last_access_dt")
    private LocalDateTime lastAccessDt;

    @Column(name = "reg_dt")
    private LocalDateTime regDt;

    public Member() {
    }

    public Member(Integer id, String name, String account, String password) {
        this.id = id;
        this.name = name;
        this.account = account;
        this.password = password;
    }
    
    //getter,setter 생략
}

 

MemberTO.java

import com.example.demo.model.entity.Member;
import java.time.LocalDateTime;

public class MemberTO {

    private Integer id;

    private String name;

    private String account;

    private String password;

    private LocalDateTime lastAccessDt;

    private LocalDateTime regDt;

    public Member toEntity() {
        return new Member(id, name, account, password);
    }
//getter,setter 생략
}

 

MemberDao.java

import com.example.demo.model.entity.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberDao extends JpaRepository<Member, Integer> {
    Optional<Member> findByAccount(String account);
}

 

MemberService.java

import com.example.demo.model.TO.MemberTO;
import org.springframework.security.core.userdetails.UserDetailsService;

public interface MemberService extends UserDetailsService {
    Integer save(MemberTO memberTO);
}

 

MemberServiceImpl.java

import com.example.demo.dao.MemberDao;
import com.example.demo.model.TO.MemberTO;
import com.example.demo.model.entity.Member;
import com.example.demo.service.MemberService;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        Optional<Member> memberEntityWrapper = memberDao.findByAccount(account);
        Member memberEntity = memberEntityWrapper.orElse(null);

        List<GrantedAuthority> authorities = new ArrayList<>();

        authorities.add(new SimpleGrantedAuthority("ROLE_MEMBER"));

        return new User(memberEntity.getAccount(), memberEntity.getPassword(), authorities);
    }

    @Transactional
    @Override
    public Integer save(MemberTO memberTO) {
        Member member = memberTO.toEntity();
        member.setLastAccessDt(LocalDateTime.now());
        member.setRegDt(LocalDateTime.now());

        // 비밀번호 암호화
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        member.setPassword(passwordEncoder.encode(member.getPassword()));
        return memberDao.save(member).getId();
    }
}

loadUserByUsernae 메소드는 입력한 account를 이용해 회원을 조회합니다. 그리고 회원 정보와 권한 정보가 담긴 User 클래스를 반환합니다. (User 클래스는 UserDetails 인터페이스를 구현하고 있습니다.)

비밀번호 인증은 SpringSecurity의 AuthenticationProvider 객체에서 진행합니다. 직접 커스텀해서 비밀번호 인증 로직을 구현할 수 있지만, 이번에는 기본적으로 지원하는 AuthenticationProvide를 사용하겠습니다.   

 

HomeController.java

import com.example.demo.model.TO.MemberTO;
import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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
@RequestMapping(value = "/")
public class HomeController {

    @Autowired
    private MemberService memberService;

    @GetMapping("/")
    public String homeView() {
        return "pages/home";
    }

    @GetMapping("/login")
    public String loginView() {
        return "pages/login";
    }

    @GetMapping("/signup")
    public String signupView() {
        return "pages/signup";
    }

    @PostMapping("/signup")
    public String signup(MemberTO memberTO) {
        memberService.save(memberTO);
        return "redirect:/login";
    }

    @PreAuthorize("hasRole('ROLE_MEMBER')")
    @GetMapping("/member/info")
    public String userInfoView() {
        return "pages/user_info";
    }

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/admin")
    public String adminView() {
        return "pages/admin";
    }

    @GetMapping("/denied")
    public String deniedView() {
        return "pages/denied";
    }
}

"src/main/resources/templates/pages/" 경로에 html 파일을 저장해주세요.

home.html - 메인화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml" 
      xmlns:sec="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>Home</title>
    </head>
    <body>
        <h1>Home Page</h1>
        <hr>
        <div>
            <a sec:authorize="isAnonymous()" th:href="@{/login}">로그인</a>
            <a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
            <a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
        </div>
        <div>
            <a th:href="@{/member/info}">내 정보</a>
            <a th:href="@{/admin}">관리자</a>
        </div>
    </body>
</html>

 

login.html - 로그인 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
        <h1>로그인</h1>
        <hr>
        <form th:action="@{/login}" method="post">
            <input type="text" name="username" placeholder="account를 입력해주세요.">
            <input type="password" name="password" placeholder="password를 입력해주세요.">
            <button type="submit">로그인</button>
        </form>
    </body>
</html>

form 요청을 할 경우 csrf 토큰을 함께 보내야합니다. 하지만 th:action으로 요청을 할 경우 자동으로 생성해줍니다.

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

th:action으로 요청하지 않을 경우 form태그 아래에 위와 같은 input 태그를 입력해야합니다.

 

signup.html - 회원가입 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>회원가입 페이지</title>
    </head>
    <body>
        <h1>회원 가입</h1>
        <hr>

        <form th:action="@{/signup}" method="post">
            <input type="text" name="account" placeholder="account 입력">
            <input type="text" name="name" placeholder="user name 입력">
            <input type="password" name="password" placeholder="password 입력">
            <button type="submit">가입하기</button>
        </form>
    </body>
</html>

 

user_info.html - 내 정보 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>내 정보</title>
    </head>
    <body>
        <h1>내 정보</h1>
        <hr>
        <span sec:authentication="name"></span> 님 반갑습니다.
        <div sec:authentication="principal.authorities"></div>

        <a th:href="@{/}">홈으로 가기</a>
    </body>
</html>

 

admin.html - 어드민 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>admin</title>
    </head>
    <body>
        <h1>admin</h1>
        <hr>
    </body>
</html>

 

denied.html - 권한 거부 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>접근 거부</title>
    </head>
    <body>
        <h1>접근 불가 페이지입니다.</h1>
        <hr>
        <a th:href="@{/}">홈으로 가기</a>
    </body>
</html>

결과 화면

관리자 페이지 진입 시


참조

https://victorydntmd.tistory.com/328

Spring Security 란?

Spring Security는 스프링 기반의 어플리케이션 보안을 담당하는 프레임워크이다.

Spring Security를 사용하면 사용자 인증, 권한, 보안처리를 간단하지만 강력하게 구현 할 수 있다. 

Filter 기반으로 동작하기 때문에 Spring MVC와 분리되어 동작한다. 

Spring Security를 이해하기 위해서는 먼저 보안관련 용어를 숙지해야 한다.

접근 주체(Principal)
보안 시스템이 작동되고 있는 애플리케이션에 접근하는 유저
인증(Authentication)
접근한 유저를 식별하고, 애플리케이션에 접근할 수 있는지 검사
인가(Authorize)
인증된 유저가 애플리케이션의 기능을 이용할 수 있는지 검사

SecurityFilterChain

일반적으로 브라우저가 서버에 데이터를 요청하면 DispatcherServlet에 전달되기 이전에 여러 ServletFilter를 거친다.

이때 Spring Security에서 등록했었던 Filter를 이용해 사용자 보안 관련된 처리를 진행한다.

Spring Security와 관련된 Filter들은 연결된 여러 Filter들로 구성되어있다. 이 때문에 Chain이라는 표현을 사용하고 있다.

SecurityFilterChain 상세

 

SecurityContextPersistenceFilter
SecurityContextRepository에서 SecurityContext를 가져와 유저 Authentication에 접근 할 수 있게 한다.
LogoutFilter
로그아웃 요청을 처리한다.
UsernamePasswordAuthenticationFilter 
ID와 Password를 사용하는 Form 기반 유저 인증을 처리한다. 
DefaultLoginPageGeneratingFilter
커스텀 로그인 페이지를 지정하지 않았을 경우 Default Login Page를 반환한다.
AnonymousAuthenticationFilter
이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환한다.
ExceptionTranslationFilter
필터 체인 내에서 발생되는 모든 예외(AccessDeniedException, AuthenticationException...)를 처리한다.
FilterSecurityInterceptor
권한부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어를 처리한다.
RequestCacheAwareFilter
로그인 성공 후, 이전 요청 정보를 재구성하기 위해 사용한다.
SessionManagementFilter
로그인 이후 인증된 사용자인지 확인하거나 설정된 Session 메커니즘에 따라 작업을 수행한다. (동시 로그인 확인 등...)
BasicAuthenticationFilter 
HTTP 요청의 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장한다. (HttpBasic 방식) 
RememberMeAuthenticationFilter
세션이 사라지거나 만료 되더라도 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리하는 필터

Spring Security 동작

1. AuthenticationFilter (UsernamePasswordAuthenticationFilter)는 사용자의 요청을 가로챈다. 그리고 인증이 필요한 요청이라면 사용자의 JSESSIONID가 Security Context에 있는지 판단한다. 없으면 로그인 페이지로 이동시킨다

로그인 페이지에서 요청이 온 경우라면 로그인 페이지에서 입력받은 username과 password를 이용해 UsernamePasswordAuthenticationToken을 만든다.  그리고 UsernamePasswordAuthenticationToken 정보가 유효한 계정인지 판단하기 위해 AuthenticationManager로 전달한다.


2. AuthenticationManager 인터페이스의 구현체는 ProviderManger이고  AuthencationProvider에게 비밀번호 인증 로직 책임을 넘긴다. (AuthencationProvider는 개발자가 직접 커스텀해서 비밀번호 인증로직을 직접 구현할 수 있다.)

3. AuthencationProvider는 UserDetailsService를 실행해 비밀번호 인증 로직을 처리한다.

UserDetailsService는 DB에 저장된 회원의 비밀번호와 비교해 일치하면 UserDetails 인터페이스를 구현한 객체를 반환하는데, UserDetailsService는 인터페이스이며 UserDetailsService를 구현한 서비스를 직접 개발해야한다.

4. 인증 로직이 완료되면 AuthenticationManager는 Authentication를 반환하며, 결과적으로 SecurityContext에 사용자 인증 정보가 저장된다.

5. 인증 과정이 끝났으면 AuthenticationFilter에게 인증에 대한 성공 유무를 전달하고
성공하면 AuthenticationSuccessHandler를 호출하고 실패하면 AuthenticationFailureHandler를 호출한다.


참조

https://sjh836.tistory.com/165?category=680970

https://okky.kr/article/382738

https://tramyu.github.io/java/spring/spring-security/

트랜잭션이란?

데이터베이스의 상태를 변경시키는 작업 또는 한번에 수행되어야하는 연산들을 의미한다.

트랜잭션 작업이 끝나면 Commit 또는 Rollback 되어야한다.


트랜잭션의 성질

원자성(Atomicity) 
한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리합니다 즉, 모두 성공 또는 모두 실패 
일관성(Consistency) 
트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
격리성(Isolation) 
동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
영속성(Durability) 
트랜잭션을 성공적으로 처리되면 결과가 항상 저장되어야한다.

@Transactional

스프링에서 지원하는 선언적 트랜잭션이다.. xml 또는 Javaconfig를 통해 설정 할 수 있다.

Spring boot에서는 별도의 설정이 필요 없으며, 클래스 또는 메소드에 선언할 수 있다.

import com.example.bamdule.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
//@Transactional(propagation = , isolation = ,noRollbackFor = ,readOnly = ,rollbackFor = ,timeout = )
public class UserServiceImpl implements UserService {
...
}

 


@Transactional 옵션

propagation
트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션이다
isolation
트랜잭션에서 일관성없는 데이터 허용 수준을 설정한다
noRollbackFor=Exception.class
특정 예외 발생 시 rollback하지 않는다.
rollbackFor=Exception.class
특정 예외 발생 시 rollback한다.
timeout
지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback 한다.  (-1일 경우 timeout을 사용하지 않는다)
readOnly
트랜잭션을 읽기 전용으로 설정한다.

1. propagation 

REQUIRED (Default)
이미 진행중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 진행중이 아니라면 새로운 트랜잭션을 생성한다
REQUIRES_NEW
항생 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션이 있다면 잠깐 보류하고 해당 트랜잭션 작업을 먼저 진행한다
SUPPORT 
이미 진행 중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 없다면 트랜잭션을 설정하지 않는다.
NOT_SUPPORT  
이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행한다.
MANDATORY  
이미 진행중인 트랜잭션이 있어야만, 작업을 수행한다. 없다면 Exception을 발생시킨다.
NEVER
트랜잭션이 진행중이지 않을 때 작업을 수행한다. 트랜잭션이 있다면 Exception을 발생시킨다.
NESTED  
진행중인 트랜잭션이 있다면 중첩된 트랜잭션이 실행되며, 존재하지 않으면 REQUIRED와 동일하게 실행된다.

TransactionSynchronizationManager.getCurrentTransactionName() 을 사용하면 간단하게 테스트해 볼 수 있다.

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private ProductService productService;

    @Override
    public void save(User user) {
    
        //트랜잭션 이름을 출력한다.
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        productService.testTransaction();
        userDao.save(user);
    }
}

 

@Service
public class ProductServiceImpl implements ProductService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void testTransaction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    }

}

 

userServiceImpl.save(user) 메소드 실행 시, 트랜잭션이 생성된다.

propagation 옵션이 없으므로 Default 옵션인  REQUIRED가 설정된다. 그리고 productService.testTransaction() 메소드가 실행되면서 새로운 트랜잭션이 생성되고 이전에 진행중이던 트랜잭션은 잠시 보류된다.

(트랜잭션 이름 출력하는 것을 보면 각각 이름이 다른 것을 볼 수 있다.) 

testTransaction() 메소드가 종료되면 보류 중이던 트랜잭션이 다시 진행 된다.

com.example.bamdule.service.impl.UserServiceImpl.save
com.example.bamdule.service.impl.ProductServiceImpl.testTransaction

다른 propagation 옵션을 설정해보면서 트랜잭션이 어떻게 전파되는지 테스트해보면 잘 이해 될 것이다.


2. isolation

Default
사용하는 DB 드라이버의 디폴트 설정을 따른다. 대부분 READ_COMMITED를 기본 격리수준으로 설정한다.
READ_COMMITED
트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 하지만 트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정 할 수 있다
그래서 트랜잭션이 같은 로우를 읽었어도 시간에 따라서 다른 내용이 발견될 수 있다.
READ_UNCOMMITED
가장 낮은 격리 수준이다.  트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출된다.
하지만 속도가 빠르기 떄문에 데이터의 일관성이 떨어지더라도, 성능 극대화를 위해 의도적으로 사용하기도 한다.
REPEATABLE_READ
트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정되는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다.
SERIALIZABLE
가장 강력한 트랜잭션 격리수준이다. 여러 트랜잭션이 동시에 같은 테이블 로우에 액세스하지 못하게 한다.
가장 안전하지만 가장 성능이 떨어진다.

3. rollbackFor

트랜잭션 작업 중 런타임 예외가 발생하면 롤백한다. 반면에 예외가 발생하지 않거나 체크 예외가 발생하면 커밋한다.

체크 예외를 커밋 대상으로 삼는 이유는 체크 예외가 예외적인 상황에서 사용되기 보다는 리턴 값을 대신해서 비즈니스 적인 의미를 담은 결과로 돌려주는 용도로 사용되기 때문이다.

스프링에서는 데이터 엑세스 기술의 예외를 런타임 예외로 전환해서 던지므로 런타임 예외만 롤백대상으로 삼는다.

하지만 원한다면 체크예외지만 롤백 대상으로 삼을 수 있다. rollbackFor또는 rollbackForClassName 속성을 이용해서 예외를 지정한다.


4. noRollbackFor

rollbackFor 속성과는 반대로 런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.


5. timeout

트랜잭션에 제한시간을 지정한다. 초 단위로 지정하고, 디폴트 설정으로 트랜잭션 시스템의 제한시간을 따른다. 

-1 입력 시, 트랜잭션 제한시간을 사용하지 않는다.


6. readOnly

트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용된다. insert,update,delete 작업이 진행되면 예외가 발생한다.


참조 : https://springsource.tistory.com/136

 

A열의 Name과 E열의 Name값을 비교해서 같으면 B열에 F열에 있는 Score값을 채우는 예제입니다

일일이 수동으로 할 수 있지만, 행이 수백개라면 수동으로 하기에는 한계가 있겠죠?

이때 사용하는 함수가 바로 INDEX & MATCH 함수입니다.

=INDEX(필요한 열 범위, MATCH(비교 열, 비교 열 범위, 0),1)
MATCH 함수의 3번째 인자 값 옵션
비교 열과 비교 열 범위가
0  : 일치
1  : 보다작음 
-1 : 보다큼 

열 범위를 표현하려면 ":" 명령어를 사용해야합니다. 예를 들어서 "B:B" 는 B열의 전체를 의미합니다.

=INDEX(F:F, MATCH(A2, E:E, 0), 1)

"F열을 필요한 값으로 지정하고, A2열과 E열 범위 검색 중 같으면 커서가 위치한 곳에 F값을 넣어주세요" 라는 의미입니다.  

B2열을 선택 후 위와 같은 명령어를 입력하고 다른 열을 클릭하면 Name A와 같은 값을 E열에서 찾아서 Score값을 반환합니다.

빨간색 동그라미를 꾹 누르고 아래로 끌어당기면 해당 함수가 나머지 열에도 적용됩니다.

+ Recent posts