티스토리 뷰
Project code in the Github{:target="_blank"}
SpringBoot Security
Spring Security Docs{:target="_blank"}
Spring Security Architecture{:target="_blank"}
Dependency
springBoot Security 사용을 위한 의존성
thymeleaf에서 security 사용을 위한 의존성
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// lombok setting
configurations {
compileOnly {
extendsFrom annotationProcessor
}
} // end lombok setting
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
// lombok dependency
implementation 'org.projectlombok:lombok:1.18.18'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Mysql
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
Properties
application.properties
# API 호출시, SQL 문을 콘솔에 출력
spring.jpa.show-sql=true
# DDL 정의시 데이터베이스의 고유 기능을 사용
spring.jpa.generate-ddl=true
# MySQL 사용
spring.jpa.database=mysql
# MySQL 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:0000/dbname?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=username
spring.datasource.password=password
# MySQL 상세 지정
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
Config
Spring Security 관련
*/config/SecurityConfig.java
import lombok.AllArgsConstructor;
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.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 // Spring Security 설정 클래스로 등록
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private MemberService memberService;
/**
* 비밀번호 암호화를 위한 Bean
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Security 설정
* @param web FilterChainProxy 생성 필터
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception
{
// Spring Security가 인증을 무시할 경로 설정
web.ignoring().antMatchers("/css/**", "/img/**", "/js/**", "/lib/**", "/vendor/**");
}
/**
* Security 설정
* @param http HTTP 요청에 대한 보안 구성
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 페이지 권한 설정
.antMatchers("/info").hasRole("MEMBER") // MEMBER, ADMIN만 접근 허용
.antMatchers("/admin").hasRole("ADMIN") // ADMIN만 접근 허용
.antMatchers("/**").permitAll() // 그외 모든 경로에 대해서는 권한 없이 접근 허용
// .anyRequest().authenticated() // 나머지 요청들은 권한의 종류에 상관 없이 권한이 있어야 접근 가능
.and() // 로그인 설정
.formLogin()
.loginPage("/user/login") // Custom login form 사용
.failureUrl("/login-error") // 로그인 실패 시 이동
.defaultSuccessUrl("/") // 로그인 성공 시 redirect 이동
.and() // 로그아웃 설정
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // 로그아웃 시 URL 재정의
.logoutSuccessUrl("/") // 로그아웃 성공 시 redirect 이동
.invalidateHttpSession(true) // HTTP Session 초기화
.deleteCookies("JSESSIONID") // 특정 쿠키 제거
.and()
// 403 예외처리 핸들링
.exceptionHandling().accessDeniedPage("/denied");
}
/**
* Spring Security 인증
* AuthenticationManagerBuilder를 사용하여 AuthenticationManager 생성
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
}
}
Controller
*/login/controller/MemberController.java
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@AllArgsConstructor
public class MemberController {
private MemberService memberService;
/**
* 메인 페이지 이동
* @return
*/
@GetMapping("/")
public String main() {
return "index";
}
/**
* 로그인 페이지 이동
* @return
*/
@GetMapping("/user/login")
public String goLogin() {
return "login/login";
}
/**
* 로그인 에러
* @param model
* @return
*/
@GetMapping("/login-error")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "/login/login";
}
/**
* 회원가입 페이지 이동
* @return
*/
@GetMapping("/signup")
public String goSignup() {
return "login/signup";
}
/**
* 회원가입 처리
* @param memberDto
* @return
*/
@PostMapping("/signup")
public String signup(MemberDto memberDto) {
memberService.joinUser(memberDto);
return "redirect:/user/login";
}
/**
* 접근 거부 페이지 이동
* @return
*/
@GetMapping("/denied")
public String doDenied() {
return "login/denied";
}
/**
* 내 정보 페이지 이동
* @return
*/
@GetMapping("/info")
public String goMyInfo() {
return "login/myinfo";
}
/**
* Admin 페이지 이동
* @return
*/
@GetMapping("/admin")
public String goAdmin() {
return "login/admin";
}
}
Entity
*/login/domain/Member.java
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "member")
public class Member implements UserDetails {
@Id
@Column(name = "id")
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "auth")
private String auth;
@Builder
public Member(String email, String password, String auth) {
this.email = email;
this.password = password;
this.auth = auth;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
for (String role : auth.split(",")) {
roles.add(new SimpleGrantedAuthority(role));
}
return roles;
}
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Role
권한 Class
*/login/domain/Role.java
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum Role {
ADMIN("ROLE_ADMIN"),
MEMBER("ROLE_MEMBER");
private String value;
}
Dto
*/login/dto/MemberDto.java
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class MemberDto {
private String email;
private String password;
private String auth;
public Member toEntity(){
return Member.builder()
.email(email)
.password(password)
.auth(auth)
.build();
}
@Builder
public MemberDto(String email, String password, String auth) {
this.email = email;
this.password = password;
this.auth = auth;
}
}
Repository
*/login/repository/MemberRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String userEmail);
}
Service
*/login/service/MemberService.java
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
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
@AllArgsConstructor
public class MemberService implements UserDetailsService {
private MemberRepository memberRepository;
/**
* 회원가입 처리
* @param memberDto
* @return
*/
@Transactional
public Long joinUser(MemberDto memberDto) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); // 비밀번호 암호화 처리
memberDto.setPassword(passwordEncoder.encode(memberDto.getPassword()));
return memberRepository.save(memberDto.toEntity()).getId();
}
/**
* 상세 정보 조회
* Security 지정 서비스이므로 필수 구현
* @param email
* @return 사용자의 계정정보와 권한을 갖는 UserDetails 인터페이스 반환
* @throws UsernameNotFoundException
*/
@Override
public Member loadUserByUsername(String email) throws UsernameNotFoundException {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException((email)));
}
}
View
src/main/resources/templates/index.html
<!DOCTYPE html>
<html
lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
>
<head>
<meta charset="UTF-8" />
<title>Main</title>
</head>
<body>
<h1>This is Main Page.</h1>
<hr />
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name"></span>님 환영합니다.
</div>
<!-- 익명의 사용자 -->
<a sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
<a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
<!-- 인증된 사용자 -->
<a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
<!-- 특정 권한의 사용자 -->
<a sec:authorize="hasRole('ROLE_MEMBER')" th:href="@{/info}">내정보</a>
<a sec:authorize="hasRole('ROLE_ADMIN')" th:href="@{/admin}">어드민</a>
</body>
</html>
src/main/resources/templates/login/login.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>Login</title>
</head>
<body>
<h1>This is Login Page.</h1>
<hr />
<!-- Security config의 loginPage("url")와 action url과 동일하게 작성-->
<form action="/user/login" method="post">
<!-- Spring Security가 적용되면 POST 방식으로 보내는 모든 데이터는 csrf 토큰 값이 필요 -->
<input
type="hidden"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"
/>
<p th:if="${loginError}" class="error">Wrong user or password</p>
<!-- 로그인 시 아이디의 name 애트리뷰트 값은 username -->
<!-- 파라미터명을 변경하고 싶을 경우 config class formlogin()에서 .usernameParameter("") 명시 -->
<input type="text" name="username" placeholder="이메일 입력해주세요" />
<input type="password" name="password" placeholder="비밀번호" />
<button type="submit">로그인</button>
</form>
</body>
</html>
src/main/resources/templates/login/signup.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>Signup</title>
</head>
<body>
<h1>This is Signup Page.</h1>
<hr />
<form th:action="@{/signup}" method="post">
<input type="text" name="email" placeholder="이메일 입력해주세요" />
<input type="password" name="password" placeholder="비밀번호" />
<input type="radio" name="auth" value="ROLE_ADMIN,ROLE_MEMBER" /> admin
<input type="radio" name="auth" value="ROLE_MEMBER" checked="checked" />
member <br />
<button type="submit">가입하기</button>
</form>
</body>
</html>
src/main/resources/templates/login/myinfo.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>MyInfo</title>
</head>
<body>
<h1>This is MyInfo Page.</h1>
<hr />
</body>
</html>
src/main/resources/templates/login/admin.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Admin</title>
</head>
<body>
<h1>This is Admin Page.</h1>
<hr />
</body>
</html>
src/main/resources/templates/login/denied.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>denied</title>
</head>
<body>
<h1>This is denied Page.</h1>
<hr />
</body>
</html>
reference
https://victorydntmd.tistory.com/328
https://shinsunyoung.tistory.com/78
https://goodteacher.tistory.com/269?category=828441
https://xmfpes.github.io/spring/spring-security/
Thymeleaf + Spring Security integration basics
'Web > Spring' 카테고리의 다른 글
[Spring Boot] File Upload (스프링 다중 파일 업로드) (0) | 2021.06.26 |
---|---|
[Spring] 스프링 핵심 원리 - 기본편 강의 노트 (0) | 2021.05.27 |
jsp를 thymeleaf로 변환하기 (convert jsp to thymeleaf) (0) | 2021.05.09 |
Spring Boot MVC 특징 (0) | 2021.04.30 |
Github에 있는 SpringBoot(maven, gradle) Project Repository import하기(STS, Eclipse) (3) | 2021.04.26 |