Spring Cecurity
Spring Security 란?
간단히 말하면 Spring의 보안(인증과 권한, 인가 등)을 담당하는 서블릿 필터의 집합이다.
서블릿 필터는 서블릿 실행 전에 실행되는 클래스들로 토큰 인증을 위해 컨트롤러 메서드의 첫 부분마다 인증 코드를 작성하는 고민을 해결하기 위해 서블릿 필터를 사용한다.
JWT(JSON Web Token) 란?
2022.10.24 - [Java] - JWT(JSON Web Token) 개념 및 구조
* 전체 Source Code *
https://github.com/kangilbin/TodoList
1. JWT 토큰 발행 방법
토큰 발행 > 로그인 시 토큰 반환 > 토큰을 이용해 API 인증
1-1) 토큰 생성을 위한 JWT 라이브러리 추가하기
https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt/0.9.0
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
- gradle 추가(또는 maven)
1-2) 토큰 클래스 생성(TokenProvider.java)
- 인증과 인가를 관리하는 패키지를 생성(security package) 후 토큰 클래스 생성
( TokenProvider.java )
@Slf4j
@Service
public class TokenProvider {
private static final String SECRET_KEY = "NMA8JPctFuna59f5";
public String create(UserEntity userEntity) {
// 기한은 지금부터 1일로 설정
Date expiryDate = Date.from(
Instant.now()
.plus(1, ChronoUnit.DAYS));
/*
* ---- 토큰 구조 ----
* { // header
* "alg" : "HS512"
* },
* { // payload
* "sub" : "40288093784915d201784916a40c0001",
* "iss" : "demo app",
* "iat" : 1595733657,
* "exp" : 1596597657
* }.
* // SECRET_KEY를 이용해 서명한 부분
* Nn4d1MOVLZg79sfFACTIpCPKqWmpZMZQ.....
*/
// JWT Token 생성
return Jwts.builder()
// header에 들어갈 내용 및 서명을 하기 위한 SECRET_KEY
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
// payload에 들어갈 내용
.setSubject(userEntity.getId()) // sub
.setIssuer("demo app") // iss
.setIssuedAt(new Date()) // iat
.setExpiration(expiryDate) // exp
.compact();
}
public String validateAndGetUserId(String token) {
// parseClaimsJws 메서드가 Base64로 디코딩 및 파싱
// 헤더와 페이로드를 setSigningKey로 넘어온 시크릿을 이용해 서명한 후 token의 서명과 비교
// 위조되지 않았다면 페이로드(Claims) 리턴, 위조라면 예외를 날림
// 그중 우리는 userId가 필요하므로 getBody를 부른다
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return claims.getSubject(); // subject 즉 사용자 아이디를 리턴한다.
}
}
- JWT 라이브러리를 사용하면 JSON을 생성, 서명, 인코딩, 디코딩, 파싱 하는 작업을 하지 않아도 된다.
1-3) 토큰을 생성한 후 반환할 DTO 생성(UserDTO.java)
( UserDTO.java )
package com.example.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UserDTO {
private String token;
private String email;
private String username;
private String password;
private String id;
}
1-4) 사용자 Controller 수정
로그인(signin) API를 호출하게 되면 토큰을 생성 및 반환한다.
(UserController.java)
// import 생략
@Slf4j
@RestController
@RequestMapping("auth")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private TokenProvider tokenProvider;
...
@PostMapping("/signin")
public ResponseEntity<?> authenticate(@RequestBody UserDTO userDTO) {
UserEntity user = userService.getByCredntials(userDTO.getEmail(), userDTO.getPassword());
// 사용자의 id, pwd 일치할 경우
if(user != null) {
// 토큰 생성
final String token = tokenProvider.create(user);
final UserDTO responseUserDTO = UserDTO.builder()
.email(user.getEmail())
.id(user.getId())
.token(token) //반환된 토큰 적용
.build();
return ResponseEntity.ok().body(responseUserDTO);
} else {
ResponseDTO responseDTO = ResponseDTO.builder()
.error("Login faild.")
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
}
}
- 로그인 (/signin) API 호출할 때 Token 인증
2. Spring Security + JWT 구현 방법
2-1) 라이브러리 설치
implementation 'org.springframework.boot:spring-boot-starter-security'
2-2) 토큰 인증 필터 생성( JwtAuthenticationFilter.java )
- 토큰을 발행할 때 만들어 두었던 security 패키지에 생성
( JwtAuthenticationFilter.java )
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 요청에서 토큰 가져오기
String token = parseBearerToken(request);
log.info("Filter is running...");
// 토큰 검사하기. JWT이므로 인가 서버에 요청하지 않고도 검증 가능
if(token != null && !token.equalsIgnoreCase(null)) {
// userId 가져오기. 위조된 경우 예외 처리된다.
String userId = tokenProvider.validateAndGetUserId(token);
log.info("Authenticated user ID : " + userId);
//인증 완료. SecurityContextHolder에 등록해야 인증된 사용자라고 생가한다.
AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userId, // 인증된 사용자의 정보. 문자열이 아니어도 아무것이나 넣을 수 있다. 보통 UserDetails라는 오브젝트를 넣는데 우리는 넣지 않았다.
null,
AuthorityUtils.NO_AUTHORITIES
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication); // 인증 정보 넣기
SecurityContextHolder.setContext(securityContext); // 다시 등록
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String parseBearerToken(HttpServletRequest request) {
// Http 요청의 헤더를 파싱해 Bearer 토큰을 리턴한다.
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
- OncePerRequestFilter라는 클래스를 상속한다. 해당 클래스는 한 요청당 반드시 한 번만 실행된다. 따라서 한 번만 인증하면 되는 필터이다.
- SecurityContext : 기본적으로 ThreadLocal에 저장되므로 Thread마다 하나의 콘텍스트를 관리할 수 있으며 같은 스레드 내라면 어디에서든 접근할 수 있다.
- JwtAuthenticationFilter 인증 흐름
1) 요청의 헤더에서 Bearer 토큰을 가져온다. 이 작업은 parseBearerToken() 메서드에서 이루어진다.
2) TokenProvider클래스를 이용해 토큰을 인증한다.
3) UsernamePasswordAuthenticationToken을 작성한다. 이 오브젝트에 사용자의 인증 정보를 저장하고 SecurityContext에 인증된 사용자를 등록한다. 등록하여 요청을 처리하는 과정에서 사용자가 인증됐는지 여부나 인증된 사용자가 누군지 알 수 있다.
3. Spring Security 설정 방법
서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려주는 설정 작업이다.
3-1) Security 설정 파일 생성( WebSecurityConfig.java )
- 설정을 관리하는 config package안에 Security 설정 클래스 생성
( WebSecurityConfig.java )
import org.springframework.beans.factory.annotation.Autowired;
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.config.http.SessionCreationPolicy;
import org.springframework.web.filter.CorsFilter;
import com.example.demo.security.JwtAuthenticationFilter;
import lombok.extern.slf4j.Slf4j;
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// http 시큐리티 빌더
http.cors() // WebMvcConfig에서 이미 설정했으므로 기본 cors 설정
.and()
.csrf() // csrf는 현재 사용하지 않으므로 disable
.disable()
.httpBasic() // token을 사용하므로 basic 인증 disable
.disable()
.sessionManagement() // session 기반이 아님을 선언
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // /와 /autho/** 경로는 인증 안해도 됨
.antMatchers("/", "/auth/**").permitAll()
.anyRequest() // /와 /auth/** 이외의 모든 경로는 인증 해야 됨
.authenticated();
// filter 등록
// 매 요청마다
// CorsFilter 실행한 후에
// jwtAuthenticationFilter 실행한다.
http.addFilterAfter(
jwtAuthenticationFilter,
CorsFilter.class
);
}
}
- HttpSecurity는 시큐리티 설정을 위한 오브젝트이다. 이 오브젝트는 빌더를 제공해 다양한 설정을 할 수 있다. 말하자면 web.xml 대신 HttpSecurity를 이용해 시큐리티 관련 설정을 하는 것이다.
- addFilterAfter() : 실제로 실행하는 것이 아니라 JwtAuthenticationFilter를 CorsFilter 이후에 실행하라고 설정하는 것이다.
4. POSTMAN을 사용한 Token 인증 구현 테스트 방법
1)
- /signup으로 아이디 생성 후 로그인 시도하여 token 발급
2)
- 정상정으로 인증됐는지 확인 Authorization > Bearer Token > Token 입력
3)
- 입력한 Token은 로그에서 확인 가능
- 토큰을 잘 못 입력하게 되면 예외 처리 발생
5. Controller 인증 적용
API 호출 시 인증 적용
( TodoController.java )
@RestController
@RequestMapping("todo")
public class TodoController {
@Autowired
private TodoService service;
@PostMapping
public ResponseEntity<?> createTodo(@AuthenticationPrincipal String userId,@RequestBody TodoDTO dto){
try {
// (1) TodoEntity로 변환한다.
TodoEntity entity = TodoDTO.toEntity(dto);
// (2) id를 null로 초기화한다. 생성 당시에는 id가 없어야하기 때문이다.
entity.setId(null);
// (3) 임시 사용자 아이디를 설정해 준다. @AuthenticaionPrincipal에서 넘어온 userId로 설정
entity.setUserId(userId);
// (4) 서비스를 이용해 Todo 엔티티를 생성한다.
List<TodoEntity> entities = service.create(entity);
// (5) 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다.
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
// (6) 변환된 TodoDTO 리스트를 이용해 ResponseDTO를 초기화한다.
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
// (7) ResponseDTO를 리턴한다.
return ResponseEntity.ok().body(response);
} catch (Exception e) {
// (8) ,혹시 예외가 있는 경우 dto 대신 error에 메시지를 넣어 리턴한다.
String error = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response);
}
}
....
- @AuthenticationPrincipal
- 스프링에서 매개변수로 넘겨준다.
- 필터(JwtAuthenticationFilter.java)에서 발급받은 토큰의 첫 매개변수를 가리킨다.
- 이 오브젝트를 SecurityContext에 등록해주고, 스프링은 해당 오브젝트를 가져와 컨트롤러 메서드에 넘겨준다.
* 즉, JwtAuthenticationFilter 클래스에서 AuthenticationPrincipal을 String형의 오브젝트로 지정했기 때문에 @AuthenticationPrincipal의 형으로 String을 사용해야 한다.
반응형
'Java' 카테고리의 다른 글
[Spring Boot] CORS 설정 방법 (0) | 2022.10.27 |
---|---|
[Spring Boot] Spring Security 패스워드 암호화 방법 (0) | 2022.10.26 |
JWT(JSON Web Token) 개념 및 구조 (0) | 2022.10.24 |
[Spring Boot] Rest API 개념 및 작성 방법 (0) | 2022.10.10 |
[Spring Boot] REST API 구현(2) DTO, Service, Controller 개념 및 작성 방법 (0) | 2022.10.09 |