JwtAuthenticationFilter : Form Login시 걸리는 Filter이다. UsernamePasswordAuthenticationFilter를 상속한 JwtAuthenticationFilter을 등록하였다. 이 필터는 HttpServletRequest에서 사용자가 Form으로 입력한 로그인 정보를 인터셉트해서 AuthenticationManager에게 Authentication 객체를 넘겨준다.
UsernamePasswordAuthenticationFilter : HttpServletRequest에서 사용자가 보낸 아이디와 패스워드를 인터셉트
1. AuthenticationFilterAnotherParam Bean 등록 2. UsernamePasswordAuthenticationFilter 등록 3. attemptAuthentication 메서드에서 넘어온 데이터를 session에 저장 4. 인증 성공 시 successfulAuthentication에서 setAuthenticationSuccessHandler를 등록 5. Controller에서(SuccessHandler에서 지정한 url) session에 저장한 데이터 사용 6. 인증 실패 시 unsuccessfulAuthentication에서 3번에서 저장했던 데이터를 Session에서 제거
API를 사용하는 웹서비스를 개발한다면, 토큰을 사용하여 유저들의 인증작업을 처리하는 것이 좋은방법
토큰기반 인증 시스템을 선택하는 이유
1. Stateless 서버
Stateful 서버 - 클라이언트에게 요청을 받을 때마다 클라이언트의 상태를 계속유지 - 해당 정보를 서비스 제공에 이용 - ex) 세션을 유지하는 웹서버 - 서버컴퓨터의 메모리 또는 데이터베이스
Stateless 서버 - 상태를 유지 하지 않는다 - 클라이언트측에서 들어오는 요청만으로 작업을 처리 - 클라이언트와 서버의 연결고리가 없기 때문에 서버의 확장성이 높아진다
2. 모바일 어플리케이션에 적합 - 안드로이드/ios에서는 쿠키는 이상적이지 않다 (쿠키 컨테이너를 사용해야하는 등) - 토큰기반은 이 번거로움을 해결할 수 있다
3.보안 - 해킹의 위험을 무조건 벗어나는 것은 아니지만 보안을 높일 수 있다
토큰기반 인증 시스템을 사용하는 서비스
트위터/ 페이스북/ 깃헙 등
토큰을 사용하게 되는 이유
1. 서버기반 인증의 문제점
세션 - 로그인 중인 유저수가 늘어나면 서버의 램이나 데이터베이스의 성능에 무리를 줄 수 있다
확장성 - 트래픽을 감당하기 위해 여러개의 프로세서를 돌리거나, 여러대의 서버 컴퓨터를 추가해야한다. 과정이 복잡해진다.
CORS - 쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계 되어있다. 따라서 쿠키를 여러도메인에서 관리하는 것은 번거롭다
토큰 기반 시스템의 작동 원리
Stateless 시스템에서 유저의 인증정보를 서버나 세션에 담아 두지 않는다
토큰기반 시스템의 구현방식
1. 유저가 아이디와 비밀번호로 로그인 2. 서버측에서 해당 계정정보를 검증 3. 계정정보가 정확하면 서버->유저 signed 토큰을 발급 4. 클라이언트 측에서 전달 받은 토큰을 저장 5. 요청시마다 토큰을 함께 서버에 전달 (http 요청의 헤더에 토큰값을 포함 시켜 전달)
토큰의 장점
무상태(Stateless) + 확장성(Scalability)
서버 확장에 적합(분산 서버라면 어떠한 서버로 요청이 들어가던 상관이 없다)
1. 보안 - 쿠키를 사용함으로 인해 발생하는 취약점이 사라짐(토큰 취약점은 존재)
2. Extensibility 확장성 - Scalability와는 다른 개념 - 로그인 정보가 사용되는 분야를 확장하는 것 - 토큰을 사용하여 다른 서비스에서도 권한을 공유 할 수 있다 (구글 네이버 등의 계정으로 내 사이트에 로그인을 할 수 있는 것처럼) - 토큰에 선택적인 권한만 부여하여 발급 할 수 있다 (프로필 정보 가져오는 권한 O / 페북에 글쓰는 권한 X)
3. 여러 플랫폼 및 도메인 - 한가지 서비스가 아닌 여러 서비스 및 도메인에서 요청이 정상적으로 처리 된다 - 어플리케이션 응답 부분에 Access-Control-Allow-Origin: * 를 포함시켜서 처리 - 이미지, css, js, html 파일 등은 CDN에서 제공, 서버측에서는 API만 다루도록 설계 가능
토큰 기반 인증 시스템의 구현체인 JWT는 웹표준에 등록되어있어서 여러 환경에서 지원이 되고 수많은 회사의 인프라스트럭쳐에서 사용 되고 있음
-----------------
JWT
기본정보
두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안전성 있게 전달
1. 수많은 프로그래밍 언어에서 지원됨
2. 자가수용적 - 필요한 모든정보를 자체적으로 지니고 있음 - 토큰의 기본정보 / 전달할 정보 / 토큰 검증 signature
3. 쉽게 전달 가능 - HTTP의 헤더나 URL의 파라미터로 전달 할 수 있음
JWT의 사용처
1. 회원인증 - 유저가 로그인 되어 있는지 신경 쓸 필요없이 유저가 요청할 때 토큰만 확인하면 됨 - 세션관리가 필요 없어서 서버 자원을 아낄 수 있음
2. 정보교류 - 두 개체 사이에 안정성 있게 정보를 교환하기에 좋은 방법 - 정보가 sign 되어 있기 때문에 정보를 보낸 이가 바뀌거나 정보가 조작되었는지 검증 가능
JWT의 생김새
JWT는 . 으로 구분하여 3가지 문자열로 되어 있음
aaaa.bbbb.cccc
(aaaa는 헤더, bbbb는 내용, cccc는 서명)
JWT 토큰을 만들때는 JWT를 담당하는 라이브러리가 자동으로 인코딩 및 해싱 작업을 해줌
헤더
헤더는 두가지 정보를 지니고 있다
typ : 토큰의 타입을 지정한다 (즉 JWT임을 알림)
alg : 해싱 알고리즘을 지정, 보통 HMAC SHA256 또는 RSA가 사용됨, 서명 부분에서 토큰을 검증할 때 사용
{
"typ": "JWT",
"alg": "HS256"
}
위 헤더내용을 공백과 엔터를 없애고 base64로 인코딩
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
정보
payload 라고 하며 토큰에 담을 정보가 들어있음. 정보들 각각을 claim이라고 부름 (name / value 의 한쌍으로 이루어져 있음)
클레임의 종류 1. 등록된(registered) 클레임 - 등록된 클레임의 사용은 선택적이다 - 서비스에서 필요한 정보가 아닌, 토큰에 대한 정보들을 담기 위하여 이름이 정해진 클레임 - iss : 토큰 발급자 - sub : 토큰 제목 - aud : 토큰 대상자 - exp : 토큰의 만료시간 (무조건 현재시간의 이후로 설정해야함) - nbf : Not Before, 토큰의 활성 날짜와 비슷한 개념, 이 날짜가 되기전엔 토큰이 처리되지 않음 - iat : 토큰이 발급된 시간, 토큰의 age를 판단 - jti : JWT의 고유식별자, 중복적인 처리를 방지하기 위해 사용, 일회용 토큰에 유용
2. 공개(public) 클레임 - 충돌이 방지된 이름을 가지고 있어야함 - 충돌을 방지하기 위해서는 클레임 이름을 URI 형식으로 지어야함
3. 비공개(private) 클레임 - 서버와 클라이언트의 협의하에 사용되는 클레임 - 공개 클레임과는 달리 이름이 중복되어 충돌 될 수 있으니 사용할 때 유의해야함
package com.jaybon.securityEx01.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.method.configuration.EnableGlobalMethodSecurity;
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.config.authentication.UserServiceBeanDefinitionParser;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.jaybon.securityEx01.config.oauth.PrincipalOauth2UserService;
@Configuration // IoC 빈(bean, 인스턴스)을 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 컨트롤러 접근 전에 낚아챔, 특정 주소 접근시 권한 및 인증 미리체크
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Bean // IoC에 등록되어 컨피그가 호출될 때 생성, 메서드를 IoC하는 방법
public BCryptPasswordEncoder enc() { // 마땅히 둘 곳이 없어서 둔 것 Controller를 제외한 곳에 둠
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/admin/1");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); // csrf 비활성화
http.authorizeRequests()
.antMatchers("/user/**").authenticated()// authenticated() 인증
// .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')") // access() 권한 .hasAnyRole()
// .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login") // 인증이 필요한 곳에 /login 으로 리다이렉트
.loginProcessingUrl("/loginProc") // 필터체인에서 인지하고 있다가 시큐리티가 낚아채서 Authentication Manager
.defaultSuccessUrl("/") // 성공하면 해당 주소로 이동 / 슬래시만 달면 이전 주소로 이동
.and()
.oauth2Login()
.loginPage("/login")
.userInfoEndpoint()
.userService(principalOauth2UserService);
}
}
(요청)
사용자 <---------> 서버
(로그인페이지)
서버에서 인증하는 것이 아니라 구글 페이스북에 먼저 인증을 요청
사용자 ----------> 구글 페이스북 ( 로그인 )
구글 페북에서 인증되면 서버에 코드를 준다
구글 페이스북 -------> 서버 (코드를 준다)
서버는 코드를 이용해서 구글 페이스북 액세스 토큰을 요청한다
토큰을 받으면 (권한을 받은 것)
서버 -------> 구글 페이스북 (토큰요청)
구글 페이스북 -------> 서버 (토큰을 준다)
액세스 토큰으로 스코프를 요청 할 수 있다
서버 -------> 구글 페이스북 (스코프요청)
구글 페이스북 -------> 서버 (사용자 정보 스코프를 준다)
일반로그인은
provider:
prividerId:
가 있는 사람은 못하게 막는다
User{
id: 자동증가값
username: 프로바이더_프로바이더 아이디
password: 임의의 문자열을 해시값으로 만들어 입력
emaill: 스코프의 이메일
provider: 프로바이더
prividerId: 프로바이더 아이디
}
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 아래에 만들어야 컴포넌트 스캔이 된다
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
8어썬티케이션은 세션에 담긴다 (Map<String, Authentication> 객체이름 = new HashMap<>();) (aa.put("Security-Context", auth)) (session.setAttribute("Security-Context-Holder", 해시맵)) 쉽게 접근 못하도록 싸매져 있다
AuthenticationManager를 이용해서Authentication를 저장할 수 있다(강제로 저장)
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("/"); // 성공하면 해당 주소로 이동 / 슬래시만 달면 이전 주소로 이동
}
}
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 내맘대로();
}