[Spring] 게시판 만들기 9 – Spring Security 등록하기 1

이제 로그인 절차를 위해 스프링 시큐리티를 사용해본다.

스프링 시큐리티외에 그냥 해도 되긴 하지만 (예전에 포폴은 그냥만든듯) 스프링 시큐리티를 사용해서 로그인 구현을 해보자.

User 관리도 로그인/로그아웃, 회원가입, 권한관리, 회원설정 변경등 다양한 기능이 있지만..

우선 로그인부터하고, 회원설정은 나중에 봐서 추가.


일단 스프링 시큐리티는 Spring Boot 들어오기전 앞단 Filter 에서 처리해준다고 한다.

https://medium.com/@greekykhs/springsecurity-part-3-spring-security-flow-7da9cc3624ab

이런 인증절차를 가지고 있고, 이 인증절차는

https://hyperskill.org/learn/step/27770

Spring MVC 앞에서 수행된다고 한다.


Spring Security 로그인 절차 등록

1. 의존성 추가

    implementation ‘org.springframework.boot:spring-boot-starter-security’

gradle 에 dependency 를 추가한다. 

2. SecurityConfig.java 생성

SpringBoot 버전에 따라 SpringSecurity Config 설정 방법이 다르니 반드시 버전을 확인해야한다.

(나는 3.3.1 임)

com/example/post 아래 config 패키지(폴더)를 생성한다.

그 아래 SecurityConfig.java 파일 생성

@Configuration

@EnableWebSecurity

public class SecurityConfig {

    @Autowired

    UserDetailService userDetailService;

    @Bean

    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http

                .authorizeHttpRequests((auth) -> auth

                        .requestMatchers("/login").permitAll()

                        // .requestMatchers("/admin").hasRole("ADMIN")

                        .requestMatchers("/WEB-INF/jsp/login.jsp").permitAll()

                        .anyRequest().authenticated()

                );

        http

                .formLogin((auth) -> auth

                        .loginPage("/login")

                        .loginProcessingUrl("/loginProc")

                        .defaultSuccessUrl("/", true)

                        .permitAll()

                );

        http  

                .csrf((auth) -> auth.disable());

        return http.build();

    }

1) 인증관련 서비스를 처리할 UserDetailService 를 Autowired 시켜준다.

2) filterChain 클래스를 만들어 Bean 으로 등록한다.

3) http 으로 시작해서 . 으로 각종 옵션을 등록하는데,

requestMatchers 를 통해 각종 pattern (URL) 을 등록할 수 있다. 또, /WEB-INF/jsp 와 같이 직접적인 웹페이지 경로를 등록할 수 도 있다.

permitAll() 은 권한이 없어도 모두 허용하겠다는 뜻이고, 주석처리된 hasRole의 경우 값으로 받는 권한(admin) 이 있는경우에만 허용한다는 뜻이다.

그 아래 formLogin 은 로그인에 관련된 설정인데, 로그인이 안된경우는 loginPage 로 이동시켜서 로그인을 수행시킨다.

csrf 는 웹 보안 취약점관련 옵션인데,  활성화하려면 추가설정이 필요해서 우선 disable

@Bean

    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();

    }

그 아래 passwordEncoder 를 등록하여 이후 DB와 연동 시 암호화 모듈을 세팅해준다.

3. User 관련 VO 객체들을 만들어준다.

User 객체

import lombok.Data;

@Data

public class User {

    private String username;

    private String password;

}

권한 Role 객체

@Data

public class Role {

    private String roleId;

    private String roleName;

}

4.  연동한 DB에도 해당 객체 테이블을 만들어준다.

CREATE TABLE user1 (
    userId INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(255) NOT NULL
);
CREATE TABLE role1 (
    roleId INT PRIMARY KEY,
    roleName VARCHAR(50) NOT NULL
);
CREATE TABLE role_user1 (
    userId INT,
    roleId INT,
    PRIMARY KEY (userId, roleId),
    FOREIGN KEY (userId) REFERENCES user1(userId),
    FOREIGN KEY (roleId) REFERENCES role1(roleId)
);

role, user 테이블과 role과 user를 매핑해주는 role_user 테이블을 만들었다.

컬럼은 이후 추가될수도 있음..

처음엔 user 테이블에 role 을쓰면되지않나 싶어서 찾아보니 (user 컬럼으로 username, password, roleId 를 갖도록)

이렇게 테이블을 하나 더 만들어서 사용하는게 더 좋은구성이라고 한다.

5. DB 연동하기

우선 서비스는 UserDetailService 로 만들것이니. Dao 부터 작성해준다.

@Repository

public class UserDao {

    @Autowired

    SqlSessionTemplate sqlSession;

    public User selectByUsername(String username) {

        return sqlSession.selectOne("UserMapper.selectByUsername", username);

    }

    public List<Role> selectRoleByUsername(String username) {

        return sqlSession.selectList("UserMapper.selectRoleByUsername", username);

    }

}

우선 username 으로 User를 얻는 쿼리, username 으로 해당 유저가 가진 권한을 가진 쿼리 두개 만들어준다.

User Mapper 를 만들것이기에 앞에 UserMapper. 를 붙여준다.

resources/mapper 에 mapper-user.xml 파일을 작성한다.

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="UserMapper">

<select id="selectByUsername" parameterType="String" resultType="com.example.post.model.User">

  SELECT * FROM USER1 WHERE username = #{username}

</select>

<select id="selectRoleByUsername" parameterType="String" resultType="com.example.post.model.Role">

  SELECT r.*

  FROM role1 r

  JOIN role_user1 ru ON r.roleId = ru.roleId

  WHERE ru.username = #{username}

</select>

</mapper>

selectRoleByUsername 에서 role 과 role_user1 테이블을 조인시켜 username 이 가지고있는 Role name 을 가져올 수 있도록 바꿔준다.

6. 인증 서비스 작성

com.example.post 아래 auth 라는 패키지(폴더)를 하나 생성한다.

그 아래 UserDetailService.java 라는 파일을 하나 만든다.

@Slf4j

@Service

public class UserDetailService implements UserDetailsService{

    @Autowired

    UserDao userDao;

    @Override

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userDao.selectByUsername(username);

        if(user == null){

            log.debug("no Username " + username);

            throw new UsernameNotFoundException(username);

        }

        List<Role> roleList = userDao.selectRoleByUsername(username);

        //roleList -> Stream 으로 변환하여 각 요소에 대해 getRoleID을 호출하여 roleId 얻어냄

        //그 결과를 toArray 해서 roles 에 넣음. String[]::new 는 Stream의 요소개수만큼 크기를 가진 String 배열을 생성하는 람다 표현식

        //메서드 참조를 이용하여 간단하게 표현

        String[] roles = roleList.stream().map(m -> m.getRoleName()).toArray(String[]::new);

        CustomUserDetails customUserDetails = new CustomUserDetails(user, roles);

        return customUserDetails;

    }

}

아까 2번에서 Autowired 시킨 UserDetailService 이다.

implements 시키면 틀이 자동으로 짜짐.

우선 username 을 받아 방금 작성한 selectByUsername 을 통해 User 를 가져와 해당 User 가 존재하는지 확인한다.

없으면 Exception 으로 반환시킴.

그리고 Role 도 가져와서 String[] roles 에 권한들을 다 집어 넣어준다.

이후 User 와 String[] roles 를 이용해 CustomUserDetails (객체) 를 작성하여 반환시킨다.

7. CustomUserDetails.java 

같은 Auth 에 CustomuserDetails.java 개체 생성

public class CustomUserDetails implements UserDetails {

    private final String username;

    private final String password;

    private final String[] roles;

    private final User user;

    public CustomUserDetails(User user, String[] roles){

        this.username = user.getUsername();

        this.password = user.getPassword();

        this.roles = roles;

        this.user = user;

    }

    //사용자 권한 Return

    @Override

    public Collection<? extends GrantedAuthority> getAuthorities() {

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

        for(String role : roles){

            authorities.add(new SimpleGrantedAuthority(role));

        }

        return authorities;

    }

    @Override

    public String getPassword() {

       return this.password;

    }

    @Override

    public String getUsername() {

        return this.username;

    }

    @Override

    public boolean isAccountNonExpired() {

        return true;

    }

    @Override

    public boolean isAccountNonLocked() {

        return true;

    }

    @Override

    public boolean isCredentialsNonExpired() {

        return true;

    }

    @Override

    public boolean isEnabled() {

        return true;

    }

    public User getUser(){

        return this.user;

    }

}

생성자를 통해 변수값을 초기화시켜줄 수 있는 객체 만들기

사용자 권한 Return은 저렇게해야한다고 함. Spring Security 방식이니 그냥 넘어가자.. 할게많아서 다 분석못함 ㅠ

8.  User 관련 작업할 컨트롤러 생성

controller 패키지에 UserController.java 를 작성해준다.

@Controller

public class LoginController {

    @GetMapping("/login")

    public String loginPage() {

        return "login";

    }

}

내용은 크게 없고 일단 login page 로 이동시키는것만 만들어둠.

9. Login Page 작성

/WEB-INF/jsp 아래 login.jsp 파일 생성

<html>

    <head>

        <meta charset="UTF-8">

        <title>Login</title>

    </head>

    <body>

        login page

        <hr>

        <form action="/loginProc" method="post" name="loginForm">

            <input id="username" type="text" name="username" placeholder="id"/>

            <input id="password" type="password" name="password" placeholder="password"/>

            <input type="submit" value="login"/>

        </form>

    </body>

    </html>

절차를 아까 securiyConfig 에서 작성한 loginProc 으로 맞춰준다.

기동하여 로그인 확인

계정생성은 미리 DB에서 진행하였고 (admin/admin) 로그인 하여 /loginProc 로 전달된 모습을 확인할 수 있다.


여기서 주요사항이 있는데,

만약 나처럼 DB에서 아이디 패스워드를 직접 insert 로 집어넣은 경우 로그인이 되지않을 수 있다.

그 이유는 SecurityConfig 에 등록한

@Bean

    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();

    }

때문인데, 이 Bean 이 password 를 암호화하여 비교하기때문이다.

지금 내 코드에는 회원가입절차가 없어서 DB에 직접 넣었기에 DB에 직접넣을때도 암호화 절차를 거쳐서 진행해야 한다.

@Autowired

private PasswordEncoder passwordEncoder;

...

        String encodedPassword = passwordEncoder.encode("admin");

        System.out.println("암호화된 비밀번호: " + encodedPassword);

로그인 절차시 실행되는 곳에 (나는 UserController 의 /login 에 넣음) 해당 코드를 넣어 암호화된 admin 값을 DB에 새로 update 하니 잘됨.


내가 로그인했는지 확인하는 방법으로

        System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());

해당코드로 현재 세션의 사용자 아이디를 가져올수있다. 나는 이걸로 확인했음.

Leave a Comment