반응형

Spring Security 사용하기 위해 다음의 의존성 추가가 필요하다. (gradle 기준)

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

 

이후 WebSecurityConfigurerAdapter을 상속받은 WebSecurityConfig클래스를 만들어준다.
*@Configuration 을 선언한 클래스에선 @Bean을 관리할 수 있다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //application.properties의 dataSource가져옴
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/", "/account/register", "/css/**").permitAll() //누구나 접근할수 있는 url
                    .anyRequest().authenticated() //어떤 요청이라도 authenticated 돼야한다.
                    .and()
                .formLogin() //authenticated안된 상태에서 다른 페이지 가려하면
                    .loginPage("/account/login") //login페이지로 redirect된다.
                    .permitAll() //login 페이지는 누구라도 접근가능
                    .and()
                .logout()
                    .permitAll();
    }

}

  

테이블 ERD는 다음과 같다.
user 테이블에는 id, username, password, enabled(활성화여부)
role 테이블은 id, name(권한명)
user와 role은 many-to-many 관계이므로 
user_role 테이블을 만들어서 관계형 테이블을 만든다.

user_id 와 role_id는 각각 pk이다.
외래키 설정을 통해 무결성을 보장하도록 한다. 

 

JDBC Authentication 설정을 위해
https://www.baeldung.com/spring-security-jdbc-authentication 을 참고했다.
-PasswordEncoder: 추후 회원가입시 암호화시켜주는 bean

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //application.properties의 dataSource가져옴
    @Autowired
    private DataSource dataSource;

  ...

    //테이블에 쿼리 날림
    //스프링 datasource 가져와서 알아서 내부에서 인증처리
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(passwordEncoder()) //알아서 비번 암호화
                .usersByUsernameQuery ("select username,password,enabled "  //인증처리
                        + "from user "
                        + "where username = ?")
                .authoritiesByUsernameQuery("select u.username, r.name " //권한처리
                        + "from user_role ur inner join user u on ur.user_id =u.id "
                        +  "inner join role r on ur.role_id = r.id "
                        + "where u.username = ?");
    }
    
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

- authoritiesByUsernameQuery: 권한정보 가져오는 쿼리, user_role에는 user와 role에 대한 정보가 없다. 따라서 user테이블과 role 테이블을 조인해서 정보를 가져오도록 한다.  

select u.username, r.name 
from user_role ur 
inner join user u on ur.user_id =u.id 
inner join role r on ur.role_id = r.id 
where u.username = ?

 

==로그인 및 회원가입 컨트롤러 설정==

@Controller
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private UserService userService;

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

    @GetMapping("/register")
    public String register(){
        return "account/register";
    }

    @PostMapping("/register")
    public String register(User user){ //modelattribute 생략해도 괜춘
        userService.save(user);
        return "redirect:/";
    }
}

==로그인시 에러처리==

login.html

<form class="form-signin" th:action="@{/account/login}" method="post">
   
   <h1 class="h3 mb-3 font-weight-normal"><a th:href="@{/}">Please sign in</a></h1>
        
        <div th:if="${param.error}" class="alert alert-danger" role="alert">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}" class="alert alert-primary" role="alert">
            You have been logged out.
        </div>
        
    <label for="username" class="sr-only">Email address</label>
    <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
    
    <label for="inputPassword" class="sr-only" name="password">Password</label>
    <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
   
    ...
</form>

- form 태그 안에서 login으로 post 요청 보내는데, 만약 에러가 발생하면(파라미터에 error발생하면) 해당 에러 메시지가 나온다(로그아웃 시에도 동일) 

 

==User, Role 모델을 만들어 many-to-many 관계설정-

참조: https://www.baeldung.com/jpa-many-to-many

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //Autoincrement 설정, IDENTITY가 많이 사용
    private Long id;

    private String username;
    private String password;
    private Boolean enabled;

    //user에 해당하는 권한이 알아서 조회돼서, roles에 담긴다.
    @ManyToMany
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name="user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roles = new ArrayList<>();
}

 

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //Autoincrement 설정, IDENTITY가 많이 사용
    private Long id;

    private String name; //권한명

    @ManyToMany(mappedBy = "roles") //mappedBy: User클래스에 있는 컬럼명이 된다.
    private List<User> users;

}

 

==UserRepository 생성==

public interface UserRepository extends JpaRepository<User, Long> {
}

==UserService 생성==

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    //유저 저장
    public User save(User user){
        // 1.비밀번호 인코딩
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);

        // 2.회원 활성화 여부 - 기본적으로 enabled로 설정
        user.setEnabled(true);

        // 3. role정보 추가
        Role role = new Role();
        role.setId(1L);
        user.getRoles().add(role);
        return userRepository.save(user);
    }
}

- DI를 통해 UserRepository 객체를 받아온다.
- WebSecurityConfig에 Bean으로 있는 PasswordEncoder를 DI로 가져와서 사용자가 전달한 비밀번호 인코딩한다.

== 로그인 여부에 따른 메뉴 구성 ==
https://www.thymeleaf.org/doc/articles/springsecurity.html
참고

common.html

<a class="btn btn-secondary my-2 mr-2 my-sm-0" th:href="@{/account/login}"
   sec:authorize="!isAuthenticated()">로그인</a>
<a class="btn btn-secondary my-2 my-sm-0" th:href="@{/account/register}"
   sec:authorize="!isAuthenticated()">회원가입</a>

<form class="form-inline my-2 my-lg-0"  th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()">
    <span class="text-white" sec:authentication="name">사용자</span>
    <span class="text-white mx-2" sec:authentication="principal.authorities">권한</span>
    <button class="btn btn-secondary my-2 my-sm-0" type="submit">로그아웃</button>
</form>

sec:authorize="!isAuthenticated() : isAuthenticated()은 로그인 되었을때니까 ! 붙여준다.
sec:authentication="name" : 로그인 한 사용자의 name 조회
sec:authentication="principal.authorities" :로그인 한 사용자의 권한 조회

이 기능을 추가하기 위해선 다음 의존성을 먼저 추가해주고, 

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

네임스페이스도 추가해준다.

+ Recent posts