----
스프링 시큐리티 보안 공부
https://spring.io/guides/gs/securing-web/
------
세션
Security-Context (key) 밑에 방대한 값(value)들이 있다
그 중에
Authentication( 인증에 필요한 권한, 유효한 이용자인지 등 모든 필드가 정해져 있다 )
필드들이 정해져 있다는 것은 만들어서 넣어야 한다는 뜻
Authentication Manager의 도움을 받아서 Authentication을 만든다
Authentication의 필수 입력 값은 (username, password)
Authentication의 구성은 정해진 필드를 따라 만들어야 한다 ( User, UserDetails )
UserDetails ( 유저네임, 패스워드, 권한 )를 만들 때 컴포지션하거나
내 커스텀 유저정보 java 파일에 UserDetails을 extends 하면 된다
접근할 때 DI를 사용하면 된다
----------------
시큐리티
요약
1. 프로젝트 생성
2. Spring Security 디펜던시 추가
3. yml 파일 설정
4. DB 생성
5. 서버 가동 및 로그인 로그아웃 테스트 (비밀번호는 콘솔창의 해시값)
6. SecurityConfig파일 생성 및 @Configuration @EnableWebSecurity 추가
6-1. WebSecurityConfigurerAdapter 상속
6-2. @EnableGlobalMethodSecurity(prePostEnabled = true) 추가
6-3. configure(HttpSecurity http) 오버라이딩
6-4. http객체에 설정하기
6-5. 폼태그의 post 요청시 거부를 당하지 않기 위해 http.csrf().disable(); 추가
새 스프링부트 프로젝트 생성
파일명 그룹 패키지 설정
spring security / spring data JPA를 세팅해서 연습해본다
템플릿을 바로추가하려면 여기서 머스태치나 타임리프를 사용
참고사항 : com.jaybon.securityEx01 아래에 만들어야 컴포넌트 스캔이 된다
properties파일을 yml파일로 변경
포트 / 컨텍스트패스 / 캐릭터셋을 설정하고 enabled (인코딩 활성화) / force(인코딩 강제활성화)
server:
port: 8080
servlet:
context-path: /
encoding:
charset: utf-8
enabled: true
force: true
jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
(security라는 데이터베이스에 접속한다는 뜻)
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
username: cos
password: cos1234
(mysql DB 생성 및 연결을 전체적으로 보려면 아래의 링크 확인)
https://blog.naver.com/getinthere/221708608489
유저와 데이터
create user 'cos'@'%' identified by 'cos1234';
GRANT ALL PRIVILEGES ON *.* TO cos@'%';
create database security;
use security;
실행 해보면 잠겨있다
루트 컨텍스트로 이동했음에도 불구하고 로그인페이지로 강제 이동한다
인터셉터에서 가로채서 막은 것이다
(각종 공격을 다 막아 준다)
필터 - 디스패쳐 - 인터셉터 - 컨트롤러
로그에 찍힌 패스워드를 입력
비밀번호는 (해시)로 되어 있다
로그아웃 하려면
http://localhost:8080/logout
----------
위의 모양이 맘에 안든다면 로그인 페이지 회원가입 페이지를 커스텀 할 것
-----------
config 패키지 생성
설정파일 하나 생성
필터 - 디스패쳐 - 인터셉터 - 컨트롤러
필터와 디스패쳐 인터셉터 사이에
시큐리티 필터 체인 ( 많은 필터가 모여있다, xss, 주소, ip, 인증 등) - 체인이 있다는 것을 기억
필터를 낚아채려면 오버라이딩 또는 extends로 재정의
전체 필터를 관리할 수 있는 클래스 (@EnableWebSecurity)
어댑터가 있다는 것은 일부 원하는 것만 오버라이딩 할 수 있다는 것
@EnableGlobalMethodSecurity(prePostEnabled = true) 를 추가하여 컨트롤러 접근 전에 낚아채기
http요청을 제어하는 함수 추가
루트컨텍스트(/)로 가는 것은 모두 허용하도록 설정
로그인 없이 바로 들어가진다
controller 패키지 추가
연습용 라우팅들 추가
다른페이지로 이동하면 권한이 없어서 페이지가 뜨지 않는다(403)
파즈티브 방식(열것만 열고 나머지는 다 잠그기)
네거티브 방식(다열고 잠글것만 잠그기) - 우리가 만들 방식
loginPage를 이용하여 인증이 필요한 상황이면 /login으로 리다이렉트한다
http://localhost:8080/admin -> http://localhost:8080/login
(파일이 없어서 404)
로그인을 리턴해주는 라우팅을 만들면
머스태치
메이븐 리파지토리에서 spring mustache 검색
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
추가해주고 서버끄고 메이븐리파지토리 업데이트 무조건!
만약 템플레이트가 적용이 안된다면 yml 파일에 아래코드 추가 (안해도 작동되지만 혹시 안된다면 사용)
mvc:
view:
prefix: /templates/
suffix: .mustache
로그인 페이지에서 로그인 요청이 오면 loginProc
템플릿에서 name을 잘 적어줘야한다
loginProc는 컨트롤러에서 낚아채는 것이 아니라 loginProcessingUrl에서 낚아채서 수행
(formLogin이라면 템플릿에서 form타입으로 날려야한다)
성공하면 해당주소로 이동 defaultSuccessUrl (Ajax와 같이 쓰지말 것 : 응답이 두번되기 때문에 꼬임)
로그인만! Ajax 쓰지말자 (회원가입은 써도됨)
@RestController를 Controller로 변경하고 데이터를 리턴하려면 @ResponseBody 추가
컨트롤러에 회원가입 라우팅 추가
템플릿에 회원가입 추가 (회원가입은 ajax로 바꾸어도 무방)
컨트롤러에 회원가입 프로세스 라우팅 추가
(SecurityConfig에서 ProcessingUrl을 안 만들었기 때문)
@RequestBody는 제이슨(또는 데이터) 일때만 사용
model 패키지 추가
유저모델추가
입력 테스트
포비든이 뜬다 무엇이 문제일까
POST일때만 폼태그로 요청 토큰(csrf-token)이 필요하다!
(csrf = 해커가 임의대로 요청을 해서 공격)
csrf-token을 임시로 만들어줘서 줘야한다
(스프링에서 csrf 토큰 만들기 검색 공부)
방법(1. csrf토큰 비활성화 // 2. 토큰 만들기)
현대 프로그래밍은 자바스크립트로 하기 때문에 csrf를 잘 당하지 않는다 (수업에선 비활성화)
SecurityConfig.java
회원가입을 하면 인덱스페이지로 이동된다
-------
JPA
연습 용이라 서비스 안만들고 레파지토리로 끝냄
JPA와 Spring Data JPA
https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/
모델에서 @Entity를 추가(데이터베이스의 모델)
IDENTITY로 설정 (오라클은 SEQUENCE를 사용, IDENTITY는 해당 DB특유의 전략 사용)
자바모델 만들어 질 때 테이블 생성
자바모델 그대로 데이터베이스에 테이블이 생성된다
(생성 create 업뎃만 update 아무것도안하려면 none)
update 만 선택해도 테이블생성까지 가능하다!
mysql에서 셀렉트해보면 만들지도 않은 테이블이 생성되어 있다
모델에서 createDate를 만들고 저장만 하면!
@CreationTimestamp 세팅 (시간 자동입력)
(java.security -> java.sql 로 만들어야한다)
DB에서 속성이 자동으로 생성되어있다
회원가입 해보면
서버를 켜보면 하이버네이트 Hibernate
(서버를 켤때 마다 지워지고 다시 만들어진다)
디폴트는 언더바 전략
네이밍 전략
커멜 표기법 전략으로 변경
jpa:
hibernate:
ddl-auto: update #create update none 3개 중 하나 선택
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
레파지토리
jpa는 어노테이션이나 컨피그 데이터 소스가 아니라 JpaRepository를 상속
제네릭 첫번째 인자는 모델클래스, 두번째 인자는 모델의 프라이머리 키의 타입을 의미한다
컨트롤러에서 DI
한줄추가
입력해보면 들어가있다
(로그인시 오류가 날것이다 암호화가 되어있지않음)
BCryptPasswordEncoder (안좋은방법 다른방법 쓸 것)
encode
SecurityConfig.java
IndexController.java
bCryptPasswordEncoder를 이용해 해시화 시켜줌
로그인
10. Authentication 객체 생성
Authentication은 AuthenticationFilter의 자식
AuthenticationFilter는 loginProc가 호출될 때 작동
UsernamePasswordAuthenticationToken 토큰이 있어야 로그인됨
https://getinthere.tistory.com/29
1아이디패스워드받아서
2토큰을받고
3유저디테일서비스가 받음
4유저디테일서비스는 유저디테일즈를 리턴
5어썬티케이션매니저가 받아서 어썬티케이션에 넣는다
6유저디테일은 시큐리티에 꼭 필요한 필드들이 있기 때문에 무조건 유저디테일로
7내가 만든 유저 모델을 유저디테일 타입으로 바꿔줘야한다
8어썬티케이션은 세션에 담긴다
(Map<String, Authentication> 객체이름 = new HashMap<>();)
(aa.put("Security-Context", auth))
(session.setAttribute("Security-Context-Holder", 해시맵))
쉽게 접근 못하도록 싸매져 있다
AuthenticationManager를 이용해서 Authentication를 저장할 수 있다(강제로 저장)
패키지 생성
유저디테일즈를 리턴해주기 위해서 클래스 하나 생성
모델 유저에 유저디테일즈를 상속받으면 유저모델이 망가지기 때문에 새로만들어줌
UserDetails를 임플리먼트해주고 오버라이딩
Authentication 객체에 저장할 수 있는 유일한 타입 - UserDetails
시큐리티 입장에서 관리해줄 수 있게 정해져 있다
User 모델 콤포지션
package com.jaybon.securityEx01.config.auth;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.jaybon.securityEx01.model.User;
import lombok.Data;
//Authentication 객체에 저장할 수 있는 유일한 타입
@Data
public class PrincipalDetails implements UserDetails{
private User user;
public PrincipalDetails(User user) {
super();
this.user = user;
}
@Override // 사용자의 비밀번호를 알고 싶으면 호출
public String getPassword() {
return user.getPassword();
}
@Override // 사용자의 유저네임를 알고 싶으면 호출
public String getUsername() {
return user.getUsername();
}
@Override // 사용자가 만료된 지를 알고 싶으면 호출
public boolean isAccountNonExpired() { // 만료안됐니?
//접속시간확인하여 true false 리턴
return true;
}
@Override
public boolean isAccountNonLocked() { // 락 안걸렸니?
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isEnabled() { // 계정활성화 되어있니?
return true;
}
@Override // 어떤 권한을 가졌니?
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return null;
}
}
UserDetailsService를 만들어야함
JPA 쿼리 짜는법
jpa 쿼리 생성룰
https://papababo.tistory.com/272
로그인 해보면 성공
-------
권한리턴하기
유저 권한 리턴하기 (주석은 Arrays)
스프링 내부에서 원하는 GrantedAuthority 타입으로만 설정해서 리스트에 저장하여 리턴
----------
구조
PrincipalDetails.java
package com.jaybon.securityEx01.config.auth;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.jaybon.securityEx01.model.User;
import lombok.Data;
//Authentication 객체에 저장할 수 있는 유일한 타입
@Data
public class PrincipalDetails implements UserDetails{
private User user;
public PrincipalDetails(User user) {
super();
this.user = user;
}
@Override // 사용자의 비밀번호를 알고 싶으면 호출
public String getPassword() {
return user.getPassword();
}
@Override // 사용자의 유저네임를 알고 싶으면 호출
public String getUsername() {
return user.getUsername();
}
@Override // 사용자가 만료된 지를 알고 싶으면 호출
public boolean isAccountNonExpired() { // 만료안됐니?
//접속시간확인하여 true false 리턴
return true;
}
@Override
public boolean isAccountNonLocked() { // 락 안걸렸니?
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isEnabled() { // 계정활성화 되어있니?
return true;
}
// Arrays.asList(new SimpleGrantedAuthority(user.getRole()));
@Override // 어떤 권한을 가졌니?
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority(user.getRole()));
return authList;
}
}
PrincipalDetailsService.java
package com.jaybon.securityEx01.config.auth;
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;
import com.jaybon.securityEx01.model.User;
import com.jaybon.securityEx01.repository.UserRepository;
// UserDetailsService는 IoC로 찾음
@Service // UserDetailsService타입으로 메모리에 뜬다 (덮어씌워짐)
public class PrincipalDetailsService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 어썬티케이션 매니저가 낚아챔
// JPA는 기본적인 CRUD만 있어서 다른걸 쓰려면 만들어줘야함
User user = userRepository.findByUsername(username);
if(user == null) {
return null;
}
return new PrincipalDetails(user);
}
}
SecurityConfig.java
package com.jaybon.securityEx01.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration // IoC 빈(bean, 인스턴스)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true) // 컨트롤러 접근 전에 낚아챔, 특정 주소 접근시 권한 및 인증 미리체크
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Bean // IoC에 등록되어 컨피그가 호출될 때 생성, 메서드를 IoC하는 방법
public BCryptPasswordEncoder enc() { // 마땅히 둘 곳이 없어서 둔 것 Controller를 제외한 곳에 둠
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); // csrf 비활성화
http.authorizeRequests()
.antMatchers("/user/**", "/admin**")
.authenticated()
.anyRequest()
.permitAll()
.and()
.formLogin()
.loginPage("/login") // 인증이 필요한 곳에 /login 으로 리다이렉트
.loginProcessingUrl("/loginProc") // 필터체인에서 인지하고 있다가 시큐리티가 낚아채서 Authentication Manager
.defaultSuccessUrl("/"); // 성공하면 해당 주소로 이동 / 슬래시만 달면 이전 주소로 이동
}
}
IndexController.java
package com.jaybon.securityEx01.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.jaybon.securityEx01.config.auth.PrincipalDetails;
import com.jaybon.securityEx01.model.User;
import com.jaybon.securityEx01.repository.UserRepository;
@Controller
public class IndexController {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping({ "", "/" })
public @ResponseBody String index() {
return "인덱스 페이지입니다";
}
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
System.out.println("확인"+principalDetails);
System.out.println(principalDetails.getUser().getRole());
System.out.println(principalDetails.getAuthorities()); // 출력 했을 때 사용자의 모든 권한을 리턴
return "유저 페이지입니다";
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "어드민 페이지입니다";
}
@GetMapping("/login")
public String login() {
return "login"; // 머스태치를 pom에 추가했으니 서픽스는 templates 프리픽스는 .mustatche
}
@GetMapping("/join")
public String join() {
return "join";
}
@PostMapping("/joinProc")
public String joinProc(User user) {
System.out.println("회원가입 진행" + user);
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
user.setRole("ROLE_USER");
userRepository.save(user);
return "redirect:/";
}
}
User.java
package com.jaybon.securityEx01.model;
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.CreationTimestamp;
import lombok.Data;
// ORM - Object Relation Mapping - 알아서 데이터베이스 테이블 만들어 준다
@Data
@Entity // 이것을 토대로 데이터베이스 모델을 만들 수 있다.
public class User {
@Id // primary key를 걸어주는 어노테이션
@GeneratedValue(strategy = GenerationType.IDENTITY) // 오라클은 시퀀스 전략, mysql은 오토인크리먼트 전략
private int id;
private String username;
private String password;
private String email;
private String role; // ROLE_USER / ROLE_ADMIN
@CreationTimestamp
private Timestamp createDate;
}
UserRepository.java
package com.jaybon.securityEx01.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.jaybon.securityEx01.model.User;
// JpaRepository를 상속하면 자동 스캔됨.
public interface UserRepository extends JpaRepository<User, Integer>{
//Jpa Naming 전략
// SELECT * FROM user WHERE username = ?
User findByUsername(String username); // 함수이름에 맞게 쿼리가 동작한다
// // SELECT * FROM user WHERE username = ? AND password = ?
// User findByUsernameAndPassword(String username, String password);
//
// @Query(value = "SELECT * FROM user", nativeQuery = true)
// User 내맘대로();
}
join.mustache
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr/>
<form action="/joinProc" method="post">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<input type="email" name="email" placeholder="email">
<button>회원가입</button>
</form>
</body>
</html>
login.mustache
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<!-- 시큐리티는 x-www-form-urlencoded 타입만 인식 -->
<form action="/loginProc" method="post">
<input type="text" name="username">
<input type="password" name="password">
<button>로그인</button>
</form>
</body>
</html>
application.yml
server:
port: 8080
servlet:
context-path: /
encoding:
charset: utf-8
enabled: true
force: true
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
username: cos
password: cos1234
# mvc:
# view:
# prefix: /templates/
# suffix: .mustache
jpa:
hibernate:
ddl-auto: update #create update none 3개 중 하나 선택
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
'Server > Spring Boot' 카테고리의 다른 글
spring boot // 페이스북 로그인 (0) | 2020.08.04 |
---|---|
spring boot // OAuth2 메모 (0) | 2020.08.03 |
spring boot // 리플렉션 (0) | 2020.08.01 |
spring boot // 필터 / 인터셉터 (0) | 2020.08.01 |
spring boot // 세팅 복습 / 시큐리티 / csrf 비활성화 / JPA / Authorities (0) | 2020.07.31 |