본문 바로가기
반응형

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

 

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

 

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());
 

 

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

728x90
반응형

한걸음 한걸음

개인적인 기록