[springboot] springboot + spring security + jpa + thymeleaf



Springboot에 Spring security를 이용하여 인증처리를 해보도록 하겠습니다. 


사용된 기술은 아래와 같습니다. 

  1.  Springboot 1.5.3

  2. Spring 4.3.9

  3. Spring Security 4.2.3

  4. Thymeleaf 2.1.5

  5. Tyhmeleaf Extras Spring Security4

  6. Embed tomcat

  7. Maven 

  8. Java 8



1. Project Directroy 





2. Project Dependencies


pom.xml


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.example</groupId>
    <artifactId>devan</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>springboot-jpa-security-login</name>
    <description>devan</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
        <relativePath /<!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
 
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
 
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
 
cs




3. Spring Security 


WebSecurityConfig.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.auth.config;
 
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.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.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private UserDetailsService userDetailsService;
  
  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder() {
      return new BCryptPasswordEncoder();
  }
  
  @Override
  protected void configure(HttpSecurity http) throws Exception{
    // h2 console 사용을 위한 설정 
    http.csrf().ignoringAntMatchers("/h2console/**");
    http.headers().frameOptions().sameOrigin();
    
    http
      .authorizeRequests()
        // 해당 url을 허용한다. 
          .antMatchers("/resources/**","/loginError","/registration","/h2console/**").permitAll()
        // admin 폴더에 경우 admin 권한이 있는 사용자에게만 허용 
          .antMatchers("/admin/**").hasAuthority("ADMIN")
          // user 폴더에 경우 user 권한이 있는 사용자에게만 허용
        .antMatchers("/user/**").hasAuthority("USER")
        .anyRequest().authenticated()
        .and()
      .formLogin()
        .loginPage("/login")
        .successHandler(new CustomAuthenticationSuccess()) // 로그인 성공 핸들러 
        .failureHandler(new CustomAuthenticationFailure()) // 로그인 실패 핸들러 
        .permitAll()
        .and()
      .logout()
        .permitAll()
        .and()
       .exceptionHandling().accessDeniedPage("/403"); // 권한이 없을경우 해당 url로 이동
  }
  
  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
  }  
}
 
cs



CustomAuthenticationSuccess.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.auth.config;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 
public class CustomAuthenticationSuccess implements AuthenticationSuccessHandler{
  private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();;
  
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      redirectStrategy.sendRedirect(request, response, "/main");
  }
}
 
cs



CustomAuthenticationFailure.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.auth.config;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 
public class CustomAuthenticationFailure implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        // 로그인 실패시 username을 화면에 그대로 출력하도록 하기 위해 아래와 같은  
        // 핸들러를 작성하였는데 더 쉬운 방법 아시는분?? 
        request.setAttribute("username", request.getParameter("username"));
        request.getRequestDispatcher("/loginError").forward(request, response);
    }
}
 
cs



4. Controller 생성

WebController.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.example.auth.web;
 
import javax.servlet.http.HttpServletRequest;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import com.example.auth.web.model.User;
import com.example.auth.web.service.SecurityService;
import com.example.auth.web.service.UserService;
 
@Controller
public class WebController {
  @Autowired
  UserService userService;
  
  @Autowired
  private SecurityService securityService;
  
  @RequestMapping(value="/main")
  public String main(){
    return "main";
  }
  
  // 로그인 
  @RequestMapping("/login")
  public String login(Model model, String error, String logout, HttpServletRequest request ){
    if (logout != null){
      model.addAttribute("logout""You have been logged out successfully.");
    }
    return "login";
  }
  
  // 로그인 실패시
  @RequestMapping(value="/loginError")
  public String loginError(Model model, String username ){
    model.addAttribute("error""Your username and password is invalid.");
    model.addAttribute("username",username);
    return "login";
  }
  
  // 회원가입폼 
  @RequestMapping(value="/registration",method=RequestMethod.GET)
  public String registration(Model model){
    model.addAttribute("userForm"new User());
    return "registration";
  }
  
  // 회원가입 처리 후 로그인 
  @RequestMapping(value="/registration",method=RequestMethod.POST)
  public String registration(@ModelAttribute("userForm") User userForm, BindingResult bindingResult, 
          Model model ,String[] roles ){
    String password = userForm.getPassword();
    userService.saveUser(userForm,roles);
    securityService.autologin(userForm.getUsername(),password);
    return "redirect:/main";
  }
  
  // admin 사용자 테스트 
  @RequestMapping("/admin")
  public String admin(){
    return "/admin/admin";
  }
  
  // user 사용자 테스트 
  @RequestMapping("/user")
  public String user(){
    return "/user/user";
  }
  
  // 권한없는 페이지를 들어갔을때 
  @RequestMapping("/403")
  public String access(){
    return "/access";
  }
}
 
cs



5. Entity Model 

User.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.auth.web.model;
 
import java.util.Set;
 
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
 
import lombok.Data;
 
@Entity
@Table(name = "user")
@Data
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
 
  private Long id;
  private String username;
  private String password;
  
  @ManyToMany(cascade=CascadeType.ALL)
  @JoinTable(name = "user_role",
             joinColumns = @JoinColumn(name = "user_id"),
             inverseJoinColumns = @JoinColumn(name = "role_id"))
  private Set<Role> roles;
}
cs



Role.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.auth.web.model;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
 
import lombok.Data;
 
@Entity
@Table(name="role")
@Data
public class Role {
  @Id
  @GeneratedValue
  private Long id;
  private String name;
 
  public Role(){
  }
  
  public Role(String name){
    this.name = name;
  }
}
cs



6. Repository


UserRepository.java


1
2
3
4
5
6
7
8
9
package com.example.auth.web.repository;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
import com.example.auth.web.model.User;
 
public interface UserRepository extends JpaRepository<User, Long>{
  public User findByUsername(String username);
}
cs



7. Service


SecurityService.java (interface)


1
2
3
4
5
6
package com.example.auth.web.service;
 
public interface SecurityService {
  String findLoggedInUsername();
  void autologin(String username, String password);
}

cs


SecurityServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.auth.web.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
 
import lombok.extern.slf4j.Slf4j;
 
@Service
@Slf4j
public class SecurityServiceImpl implements SecurityService {
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Override
    public String findLoggedInUsername() {
        Object userDetails = SecurityContextHolder.getContext().getAuthentication().getDetails();
        if (userDetails instanceof UserDetails) {
            return ((UserDetails) userDetails).getUsername();
        }
        return null;
    }
 
    @Override
    public void autologin(String username, String password) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken 
         = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); 
 
        authenticationManager.authenticate(usernamePasswordAuthenticationToken);
 
        if (usernamePasswordAuthenticationToken.isAuthenticated()) {
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            log.debug(String.format("Auto login %s successfully!", password));
        }
    }
}
cs



UserService.java (interface)

1
2
3
4
5
6
7
8
package com.example.auth.web.service;
 
import com.example.auth.web.model.User;
 
public interface UserService {
  void saveUser(User user,String[] roles);
  User findByUsername(String username);
}
cs



UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.auth.web.service;
 
import java.util.HashSet;
import java.util.Set;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
 
import com.example.auth.web.model.Role;
import com.example.auth.web.model.User;
import com.example.auth.web.repository.UserRepository;
 
import lombok.extern.slf4j.Slf4j;
 
@Service
@Slf4j
public class UserServiceImpl implements UserService {
  @Autowired
  private UserRepository userRepository;
  
  @Autowired
  private BCryptPasswordEncoder bCryptPasswordEncoder;
    
  @Override  
  public void saveUser(User user,String[] roles) {
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
    Set<Role> rolesSet = new HashSet<Role>();
    for(String role:roles){
      rolesSet.add(new Role(role));
    }
    user.setRoles(rolesSet);
    userRepository.save(user);
  }
  @Override
  public User findByUsername(String username) {
    return userRepository.findByUsername(username);
  }
}
cs


UserDetailsServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.auth.web.service;
 
import java.util.HashSet;
import java.util.Set;
 
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.stereotype.Component;
 
import com.example.auth.web.model.Role;
import com.example.auth.web.model.User;
import com.example.auth.web.repository.UserRepository;
 
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        
        for (Role role : user.getRoles()) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
        }
 
        return new org.springframework.security.core.userdetails.User(user.getUsername(), 
                                                                      user.getPassword(),
                                                                      grantedAuthorities);
    }
}
cs



8. thymeleaf


login.html


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Login</title>
</head>
<body>
    login
    <form th:action="@{/login}" method="post">
        name: <input type="text" name="username" id="username"
            th:value="${username}" /><br />
        p w: <input type="password" name="password" id="password" /><br />
        <div th:if="${logout}">You have been logged out successfully</div>
        <input type="submit" value="sign in" />
    </form>
    <a href="/registration">registration</a>
</body>
</html>

cs



registration.html


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Registration</title>
</head>
<body>
Registration
<form name="userForm" th:action="@{/registration}" method="post">
  name : <input type="text" name="username" id="username"/>
  password : <input type="text" name="password" id="password"/>
  <br/>
  ADMIN <input type="checkbox" name="roles" value="ADMIN"/> | 
  USER  <input type="checkbox" name="roles" value="USER" />
  <hr/>
  <input type="submit" value="join" />
</form>
 
</body>
</html>
cs




main.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8" />
<title>Main</title>
</head>
<body>
    <span sec:authentication="name"></span>
    <span sec:authentication="principal.authorities"></span> 
 
    <div sec:authorize="hasAuthority('ADMIN')">- only admin!!!!</div>
    <div sec:authorize="hasAuthority('USER')">- only user</div>
 
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
    
    <a th:attr="href=@{/admin}">admin</a>
    <a th:attr="href=@{/user}">user</a>
</body>
</html>
cs



access.html


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title></title>
</head>
<body>
<script type="text/javascript">
<!--
// 403에러메세지가 발생했을 경우 이전페이지로이동한다. 
history.back(-1);
//-->
</script>
</body>
</html>
cs



9.Application.properties


1
2
3
4
5
6
7
8
9
server.port=1322
 
hibernate.hbm2ddl.auto=update
 
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:~/db1;DB_CLOSE_ON_EXIT=FALSE
 
spring.h2.console.enabled=true
spring.h2.console.path=/h2console
cs



10.demo



로그인 화면

로그인에 실패 할 경우 아래와 같이 표시

ADMIN권한으로 회원가입

회원가입후 name과 권한 표시

admin 링크를 눌러 admin 페이지로 들어가는지 확인(user 링크를 누를 경우 다시 main으로 돌아온다)

USER권한으로 회원가입 

이름과 USER 권한을 확인할 수 있다.

마찬가지로 user 페이지로 이동하여 정상 출력되는지 확인 (admin에 경우 권한이 없으므로 main으로 이동)

마지막으로 모든권한을 가진 USER를 생성해본다

ADMIN,USER 권한 모두 가진걸 볼수 있다. admin,user 페이지 모두 들어가지는 걸 확인할 수 있다. 

h2console로 들어가 DB에 입력된 내용을 확인해 보자

비밀번호가 암호화 되어 들어가 있는것을 확인 할 수 있다.



11. Conculution


SpringSecurity와 JPA를 이용하여 간단한 로그인 인증과 권한 설정으로 로그인 처리와 특정페이지만 접속이 가능하도록 설정해 보았다. 본문에 설명은 하지 않았지만 html form 마다 _csrf 값이 자동으로 주입되며 서버에 요청이 갈때 해당값을 넘기도록 하여 사이트 간 요청 위조을 막아준다. 기존에 개발자가 직접해야 할 일들을 security에서 많은 처리를 해준다는 사실을 알수 있다. 소스는 아래 github에서 확인 할 수 있다. 


https://github.com/beans9/springboot-security-jpa-thymeleaf



12. Reference


https://hellokoding.com/registration-and-login-example-with-spring-security-spring-boot-spring-data-jpa-hsql-jsp/

http://www.mkyong.com/spring-boot/spring-boot-spring-security-thymeleaf-example/