1. 기본 환경 설정
1) https://start.spring.io/ 에서 spring initializr 를 사용하여 프로젝트를 구성하고 GENERATE로 생성한다
2) IntelliJ를 열고 프로젝트 파일을 오픈한다
- 생성된 파일을 지정 폴더에 압축풀기를 한다
- 압축을 푼 폴더에서 build 파일을 intelliJ에서 오픈한다
3) 의존성을 설정한다
- build.gradle 파일을 열고 아래와 같이 코드를 작성한다
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.codestates'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-mustache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'com.h2database:h2'
}
tasks.named('test') {
useJUnitPlatform()
}
- gradle 에서 코드를 추가하거나 수정한 후에는 Gradle 탭에서 Reload를 해 준다
3) db.jpa 설정을 추가한다
- resource 폴더 아래에 application.yml 파일을 추가한다
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create
show-sql: true
4) 애플리케이션 출력 화면을 구성하기 위해 2개의 파일을 추가한다
- src/resources/templates 아래에 추가한다
- loginForm.html
- 로그인 화면에 출력되는 HTML 코드이다
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr />
<form action="/login" method="post">
<input type="text" name="username" placeholder="Username" /><br />
<input type="password" name="password" placeholder="Password" /><br />
<button>로그인</button>
</form>
<a href="join">회원가입</a>
</body>
</html>
- joinForm.html
- 회원가입 화면에 출력되는 HTML 코드이다
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr />
<form action="/join" method="post">
<input type="text" name="username" placeholder="Username" /><br />
<input type="password" name="password" placeholder="Password" /><br />
<input type="email" name="email" placeholder="Email" /><br />
<button>회원가입</button>
</form>
</body>
</html>
2. 기본 실행
1) 애플리케이션을 실행한다
- intelliJ에서 프로젝트를 실행한다
- 콘솔 화면에 아래와 같이 password가 부여되고 설명이 출력되면 기본 설정이 적용된 것이다
- http://localhost:8080 으로 접속한다
- Username : user / password : 콘솔창에 표시된 password 를 작성한다
- 현재 login 화면 구성이 없기 때문에 Error 메시지가 출력될 것이다
3. Spring Security Configuration 적용
1) src > main > java > com.memberlogin.loginjoin 아래에 controller 패키지와 IndexController.java를 생성한다
- 코드 생성 후 반드시 애플리케이션을 재실행 한다
package com.memberlogin.loginjoin.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@GetMapping("/")
public @ResponseBody String index() {
return "index";
}
@GetMapping("/user")
public @ResponseBody String user() {
return "user";
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "admin";
}
@GetMapping("/manager")
public @ResponseBody String manager() {
return "manager";
}
@GetMapping("/login")
public @ResponseBody String login() {
return "login";
}
@GetMapping("/join")
public @ResponseBody String join() {
return "join";
}
}
- 애플리케이션을 재실행 후 web 접속하여 로그인을 하면 아래와 같이 출력된다
- url을 지정하지 않은 경우에 출력은 'index'로 설정되어 있다
- 총 5개의 url을 설정하였고 5개 중 1개의 url을 제외하고는 모두 로그인 시 정상적으로 작동하는 것을 확인할 수 있다
- /login의 경우에는 Spring Security가 처리하고 있기 때문에 작동하지 않는다
localhost:8080/login | localhost:8080/user | localhost:8080/admin | localhost:8080/manager | localhost:8080/join |
![]() |
![]() |
![]() |
![]() |
![]() |
2) src > main > java > com.memberlogin.loginjoin 아래에 config 패키지와 SecurityConfig.java를 생성한다
- @Configuration과 @EnableWebSecurity를 추가한다
- @EnableWebSecurity 추가 시 스프링 시큐리티 필터가 스프링 필터체인에 등록 된다 - filterChain 메서드를 @Bean으로 등록한 후 스프링 컨테이너에서 관리할 수 있도록 한다
- http.csrf().disable(); 의 경우에는 form 태그로만 요청이 가능해지고 postman등의 요청이 불가능하게 된다
- csrf를 disable 한다 - http.headers().frameOptions().disable(); 은 h2 연결할 때 필요하다
- Config 설정이 되면 /login에 접속이 가능하게 된다
package com.memberlogin.loginjoin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
return http.build();
}
}
- 애플리케이션을 재실행 후 web 접속하여 로그인을 하면 아래와 같이 출력된다
- http://localhost:8080 으로 접속하면 index 가 표시되고
- http://localhost:8080/login 으로 접속하면 login 이 표시된다 - /, /login, /join 3개의 url은 로그인 없이도 접속이 가능하다
- /admin, /manager url의 경우에는 권한이 없기 때문에 403 에러가 출력된다
- 아래 코드를 추가한 후 재실행하고 접속하면 로그인이 가능해 진다
- admin 과 manager 로 접속 시 login이 출력된다
.and()
.formLogin()
.loginPage("/login");
3) config 패키지 아래에 WebMvcConfig.java를 생성한다
- mustache → html 사용할 수 있도록 설정한다
package com.memberlogin.loginjoin.config;
import org.springframework.boot.web.servlet.view.MustacheViewResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
MustacheViewResolver resolver = new MustacheViewResolver();
resolver.setCharset("UTF-8");
resolver.setContentType("text/html; charset=UTF-8");
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
registry.viewResolver(resolver);
}
}
- ViewResolver 구현 클래스 종류
4) src > main > java > com.memberlogin.loginjoin 아래에 model 패키지와 Member.java를 생성한다
package com.memberlogin.loginjoin.model;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDateTime;
@Entity
@Data
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
private String role;
private LocalDateTime createdAt = LocalDateTime.now();
}
5) src > main > java > com.memberlogin.loginjoin 아래에 repository 패키지와 MemberRepository.java를 생성한다
package com.memberlogin.loginjoin.repository;
import com.memberlogin.loginjoin.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
4. 회원 가입
1) 회원가입에 필요한 코드를 수정한다
- controller 패키지의 IndexController 클래스를 수정한다
- 코드를 수정하고 실행하면 정상적으로 db에 저장되게 된다
@Controller
public class IndexController {
@Autowired
MemberRepository memberRepository;
...
@GetMapping("/login")
public String login() {
return "loginForm";
}
@GetMapping("/join")
public String joinForm() {
return "joinForm";
}
@PostMapping("/join")
public @ResponseBody String join(Member member) {
member.setRole("ROLE_USER");
memberRepository.save(member);
return "join";
}
}
- 애플리케이션을 재실행하면 아래와 같이 로그인 페이지가 정상적으로 출력되고 로그인 시 index 가 출력된다
![]() |
![]() |
2) 패스워드 암호화를 위한 코드 수정
- config 패키지의 SecurityConfig 클래스를 수정한다
- 아래 이미지에 @Bean 이 한 곳 빠져 있다...이 오류 때문에 고생 많이 했다...그래서 이미지를 수정하지 않고 둔다..^^
- @Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
-> IndexController 클래스의 Bean 충돌로 인하여 서버 실행에 에러가 발생한다.
-> 주석 처리해 주면 해결된다
public class SecurityConfig {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
...
@Bean // 추가
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
- controller 패키지의 IndexController 클래스를 수정한다
@PostMapping("/join")
public String join(Member member) {
member.setRole("ROLE_USER");
String rawPassword = member.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
member.setPassword(encPassword);
memberRepository.save(member);
return "redirect:/login";
}
- 애플리케이션 재실행 후 웹에 접속하여 확인한다
![]() |
![]() |
- http://localhost:8080/h2 에 접속하여 입력된 데이터를 확인할 수 있다
5. 로그인
1) config > auth 패키지를 만들고 PrincipalDetails 클래스를 생성한다
- PrincipalDetails 클래스에 implements UserDetails와 메서드를 오버라이드 한다
- org.springframework.security.core.userdetails
- 보안 목적으로 Spring Security에서 직접 사용되지 않는다
- 단순히 나중에 객체로 캡슐화되는 사용자 정보를 저장 Authentication 한다
- 보안과 관련되지 않은 사용자 정보(예: 이메일 주소, 전화번호 등)를 편리한 위치에 저장할 수 있다
org.springframework.security.core.userdetails (spring-security-docs 5.7.2 API)
docs.spring.io
- 시큐리티는 /login 주소에 요청이 오면 대신 로그인을 진행한다
- Authentication 타입 객체이며 안에 Member 정보가 있어야 한다
- 로그인 진행이 완료되면 security session을 만들어 준다 (Security ContextHolder)
package com.memberlogin.loginjoin.config.auth;
import com.memberlogin.loginjoin.model.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class PrincipalDetails implements UserDetails {
private Member member;
public PrincipalDetails(Member member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
//isCredentialsNonExpired()는 암호 사용 기간이 지났는지 여부를 확인한다
return true;
}
@Override
public boolean isEnabled() {
//isEnabled()은 특정 사이트 규칙에 따라 return false로 설정한다
// ex) 1년 동안 로그인을 하지 않았을 경우
return true;
}
}
//현재 따로 규칙이 없기 때문에 isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled 메서드들을
//모두 return true로 설정한다
2) config > auth 패키지에 PrincipalDetailsService 클래스를 생성한다
- PrincipalDetailsService에 implement UserDetailsService와 메서드를 오버라이드 한다
- Security 설정에서 loginProcessingUrl(”/login”);으로 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행된다
- 메서드 파라미터에 username이라고 되어있으면 form을 통해 username을 가져올 때 name이 반드시 매치되야 한다
- 이름을 똑같이 변경해주거나
- SecurityConfig에 .loginPage() 아래에 .usernameParameter(”다른 이름")으로 추가해야 한다 - loadUserByUsername 함수가 Authentication으로 값이 return 된다
package com.memberlogin.loginjoin.config.auth;
import com.memberlogin.loginjoin.model.Member;
import com.memberlogin.loginjoin.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member memberEntity = memberRepository.findByUsername(username);
System.out.println("username : " + username);
if(memberEntity != null) {
return new PrincipalDetails(memberEntity);
}
return null;
}
}
3) MemberRepository에 findByUsername 메서드를 추가해줍니다.
- PrincipalDetailsService에 있는 loadUserByUsername에서 사용하기 때문에 해당 메서드를 추가한다
- UserDetailService에서 findByUsername을 구현처리 할 때 자동적으로 생성되지만 한번 더 확인한다
package com.memberlogin.loginjoin.repository;
import com.memberlogin.loginjoin.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
public Member findByUsername(String username);
}
- 애플리케이션을 재실행 한 후 웹에 http://local:8080/uesr 접속하여 회원 가입 후 로그인을 진행한다
- 이상없이 회원가입과 로그인이 진행된다
- 친절하게 비밀번호에 대한 확인 창도 표시된다
![]() |
![]() |
- localhost:8080/login 과 localhost:8080/join, localhost:8080/user 은 원활하게 접속이 된다
![]() |
![]() |
로그인을 실행한 후 화면이다 로그아웃은 하지 않았다 ![]() |
- localhost:8080/admin 과 localhost:8080/manager는 다시 403 에러가 뜨는 것을 확인할 수 있다
![]() |
![]() |
6. 권한 처리
1) manager, admin 처리
- 회원가입 후 h2 database에서 ROLE을 ROLE_ADMIN으로 변경해줍니다.
- localhost:8080/admin 과 localhost:8080/manager 에 정상적으로 접근이 가능하고 그 외 모든 url 또한 접근이 가능한 것을 확인할 수 있다
- SecurityConfig 클래스에 코드를 추가한다
@Configuration
@EnableWebSecurity
//권한 처리 추가
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
/*@EnableGlobalMethodSecurity(securedEnabled = true)
- Secured 애너테이션이 활성화 된다
- SecurityConfig에서 접근 권한 설정이 아닌 Controller에서 애너테이션으로 관리할 수 있게 된다*/
/*@EnableGlobalMethodSecurity(prePostEnabled=true)
- PreAuthorize, PostAuthorize 애너테이션이 활성화 된다.*/
public class SecurityConfig {
2) IndexController.java에 새로운 메서드와 @Secured와 @preAuthorize를 추가한다
//권한 추가
@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info() {
return "info";
}
@PostMapping("/join")
public String join(Member member) {
member.setRole("ROLE_USER");
String rawPassword = member.getPassword
- SecurityConfig에 .antMatchers("/info/**").access("hasRole('ROLE_ADMIN')") 코드를 추가하는 것과 같은 동작이 된다
- @Secured는 1개의 권한을 주고 싶을 때 사용한다
- @PreAuthorize는 1개 이상의 권한을 주고 싶을 때 사용한다
: #을 사용하면 파라미터에 접근할 수 있다
: ex) @PreAuthorize("isAuthenticated() and (( #user.name == principal.name ) or hasRole('ROLE_ADMIN'))")
- @PostAuthorize는 메서드가 실행되고 응답하기 직전에 권한을 검사하는데 사용된다
- 클라이언트에 응답하기 전에 로그인 상태 또는 반환되는 사용자 이름과 현재 사용자 이름에 대한 검사, 현재 사용자가 관리자 권한을 가지고 있는지 등의 권한 후처리를 할 수 있다
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
@GetMapping("/data")
public @ResponseBody String data() {
return "data";
}
※ 참조 링크
BCryptPasswordEncoder (spring-security-docs 5.7.2 API)
All Implemented Interfaces: PasswordEncoder public class BCryptPasswordEncoder extends java.lang.Object implements PasswordEncoder Implementation of PasswordEncoder that uses the BCrypt strong hashing function. Clients can optionally supply a "version" ($2
docs.spring.io
'인증 & 보안' 카테고리의 다른 글
웹 보안 공격 (0) | 2022.07.23 |
---|---|
보안 - Hashing,Coolie,Seesion (0) | 2022.07.23 |
인증 - HTTPS (0) | 2022.07.21 |