일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 다이나믹 프로그래밍
- dfs
- 알고리즘
- TCP
- 컴퓨터 네트워크
- 도커
- SQL
- 분할 정복
- 재귀
- 다이나믹프로그래밍
- 그리드
- 브루트포스
- CI/CD
- BFS
- GIT
- AWS
- 그래프
- 자료구조
- 이분탐색
- 분할정복
- 그리드 알고리즘
- 자바
- 트리
- Spring
- 스프링
- 백준
- 순열
- github action
- HTTP
- 역방향 반복자
- Today
- Total
코딩성장스토리
Spring Security 공부 + Jwt 적용 본문
팀 프로젝트를 하면서 여태껏 추상적으로만 알았던 개념들을 구체화 시킨 것들이 많아 졌다.
그 중에 하나가 Security이다.
일단 Spring Security를 공부하면서 당연하지만 잊지 말아야 하는건 Security는 우리의 편의를 위해 만들어진 것이다.
그러니 스트레스 받지 말장... (시큐리티 버전이 업그레이드 되면서 메서드가 많이 달라짐...😩)
난 처음에 이 사진을 보고 이게 도대레 뭔 소리인지 감이 안잡히고 스트레스 받았었다... 😂
하지만 공부를 하고 난 후 이 그림은 그냥 완벽한 그림이다...
가장 이해가 안되고 헷갈렸던 부분은 UserDetails이다... 아니 User라는 객체를 만들었는데 UserDetails는 뭘까...
이것 또한 개발의 편의를 위해 만들어진 것이다. 아래 코드가 UserDetails를 구현한 것이다.
@Getter
@RequiredArgsConstructor
public class PrincipalUserDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(RoleType.USER.getKey()));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
각 오버라이드 된 메서드들은 이름만 보면 쉽게 유추가 가능하다. 그런데 Authorities란 뭘까?
이건 우리가 임의로 유저에게 역할을 정해주는 것이다. 즉 일반 사용자 (ROLE_USER)가 있을 수도 있고 관리자(ROLE_ADMIN) 같은 역할들이 나누어 질 수 있다. 그럼 SecurityConfig를 만들어 구분짓게 할 수 있다.. 아래 코드를 보자
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean //해당 메서의 리턴되는 오브젝트를 ioc로 등록해준다
public BCryptPasswordEncoder encoderPwd(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()//admin만 들어 갈 수 있음
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("").authenticated()
.requestMatchers("/").authenticated()
.requestMatchers("/user/**").authenticated() //권한이 있는 자만 들어올 수 있음
.requestMatchers("/admin/**").hasRole("ADMIN") //시큐리티는 role변수에 ROLE_* 규칙으로 적어야함 그리고 hasRole()에는 ROLE_뺴고 뒤에 적어야
.anyRequest().permitAll() //이외의 url은 모든 사람 접근 허용
)
return http.build();
}
}
즉 저 위에서 hasRole("ADMIN")을 보듯이 우리가 UserDetails에 저장된 권한을 보고 시큐리티가 알아서 걸러준다!
그럼 이제 궁금한 게 authenticated() 이 부분이다! 사실 Spring Security는 기본적으로 /login url을 지원해 준다.
그리고 loginpage를 따로 지정 할 수 있지만 일단 구조를 보기때문에 넘어가자
다시 이 그림을 보자 . UserDetailService를 보자
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
//시큐리티 session(내부 Authentication(내부 UserDetails))
//함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("----");
User user = userRepository.findByUsername(username);
System.out.println(user);
if(user != null){
return new PrincipalDetails(user);
}
return null;
}
}
저기 loadUserByUsername이 로그인 할때 들어오는 username과 password를 DB값과 비교해주는 메서드 이다.
이것도 디폴트 값으로 /login 이 호출되면 실행 된다.
난 Jwt로 로그인을 구현 할 생각이다. 그럼 UsernamePasswordAuthenticationFilter 를 상속받아 필터를 추가하자
UsernamePasswordAuthenticationFilter 이란? 시큐리티필터체인에 있는 필터중 하나이고 유저 인증할때 쓰이는 필터이다.
나는 json으로 로그인 정보를 받을려고 한다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private ObjectMapper mapper = new ObjectMapper();
//로그인 시도시 이 메서드 실행해서 Authentication에 담음
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("----------------------------");
//Json으로 받기
ServletInputStream inputStream = null;
LoginDto helloData = null;
try {
inputStream = request.getInputStream();
String msgBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("msg Body = " + msgBody);
helloData = mapper.readValue(msgBody, LoginDto.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
String email = helloData.getEmail();
String password = helloData.getPassword();
System.out.println(email + " " +password);
String encodedPassword = passwordEncoder.encode(password);
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
PrincipalUserDetails principalDetailis = (PrincipalUserDetails) authentication.getPrincipal();
System.out.println("Authentication : "+principalDetailis.getUser().getEmail());
return authentication;
}
}
그리고 이 필터를 보면 authenticationManager가 보이고 이는 위에서 보던 구조 그림에서 나온 것이다.
인증은 SpringSecurity의 AuthenticatonManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider(구조그림에서 authenticationManager 다음에 나오는 것) 에 의해 처리된다. 인증이 성공하면 2번째 생성자를 이용해 인증이 성공한(isAuthenticated=true) 객체를 생성하여 Security Context에 저장
그럼 AuthenticationProvider도 구현해보자 (구현안해도 돌아가긴하는데 내가 임의로 짤 수 있음)
@Component
//Authentication에 담은 정보를 DB와 일치하는지 체크
public class UserAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
String password = (String) authentication.getCredentials();
PrincipalUserDetails userDetails = (PrincipalUserDetails) userDetailsService.loadUserByUsername(email);
// 비밀번호 확인
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
}
//이 토큰이 곧 인증 객체
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
아까 봤던 구조에서 밑줄 친 곳 과 화살표를 보자 . 우리는 UsernamepasswordAuhtenticaiton을 통해 인증을 했다.
그럼 그 결과에 대한 값이 있어야 되야 한다. 그걸 위한게 저 AuthenticaitonSuccessHandler 와 AuthenticaitonFailureHandler
이다.
AuthenticaitonSuccessHandler
@RequiredArgsConstructor
@Component
//로그인 성공시 처리 클래스
public class UserSuccessHandler implements AuthenticationSuccessHandler {
private final UserRepository userRepository;
private final JwtService jwtService;
@Transactional
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
PrincipalUserDetails userDetails = (PrincipalUserDetails) authentication.getPrincipal();
User user = userRepository.findByEmail(userDetails.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("가입된 이메일이 존재하지 않습니다."));
// JWT Token 생성 & Response
String accessToken = jwtService.createAccessToken(user.getEmail(), user.getId());
String refreshToken = jwtService.createRefreshToken(user.getEmail());
user.updateRefreshToken(refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
LoginTokenRes loginTokenRes = new LoginTokenRes(accessToken,refreshToken,signYN);
new ObjectMapper().writeValue(response.getWriter(), new BaseResponse(loginTokenRes));
}
}
성공하면 jwt 반환!
그럼 여기서 궁금점?
로그인을 하는 것은 좋다. 그럼 로그인해서 얻은 jwt로 다른 화면도 들어갈텐데 그때도 필터가 필요하지 않을까?
이 해결법은 간단하다.
일단 jwt를 가지고 인증하는 필터를 만든다 .
securityConfig의
filterchain 메서드에 http.addFilter(new JwtAuthorizationFilter(authenticationManager,userRepository)); 를 추가해
인증 전에 내가 만든 필터를 거치게 한다.
코드를 보자
로그인 후 jwt가지고 접근 할 때 필터
// 인가 BasicAuthenticationFilter 권한 인증 들어올때 실행됨
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(JwtProperties.HEADER_STRING);
if(header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
System.out.println("header : "+header);
String token = request.getHeader(JwtProperties.HEADER_STRING)
.replace(JwtProperties.TOKEN_PREFIX, "");
// 토큰 검증 (이게 인증이기 때문에 AuthenticationManager도 필요 없음)
// 내가 SecurityContext에 집적접근해서 세션을 만들때 자동으로 UserDetailsService에 있는 loadByUsername이 호출됨.
String username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token)
.getClaim("username").asString();
if(username != null) {
User user = userRepository.findByUsername(username);
// 인증은 토큰 검증시 끝. 인증을 하기 위해서가 아닌 스프링 시큐리티가 수행해주는 권한 처리를 위해
// 아래와 같이 토큰을 만들어서 Authentication 객체를 강제로 만들고 그걸 세션에 저장!
PrincipalDetails principalDetails = new PrincipalDetails(user);
Authentication authentication =
new UsernamePasswordAuthenticationToken(
principalDetails, //나중에 컨트롤러에서 DI해서 쓸 때 사용하기 편함.
null, // 패스워드는 모르니까 null 처리, 어차피 지금 인증하는게 아니니까!!
principalDetails.getAuthorities());
// 강제로 시큐리티의 세션에 접근하여 값 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
위 코드를 보면 헤더에 아무 것도 없으면 이 필터는 거치지 않게 한다-> 로그인 진행
헤더에 jwt가 있으면 로그인이 된 것이므로 적절한 jwt인지 검증 거치게 한다.
요약
1 .Security는 우리가 만든 User 객체에 Userdetail을 감싸고 그 위에 authentication을 감싸고 인증이 이루어 진다.
2 .인증은 시큐리티 안에 있는 필터인 UsernamepasswordAuhtenticaitonFilter를 만들고 그 안에서 authenticationManager 을 이용한 인증을 한다. (이 안에서 AuthenticationProvider을 이용한 인증 그리고 그 안에 UserDetailService 안에 정의된 loadUserByUsername를 통한 db값 비교) 이 과정을 통한 로그인 인증이 된다.
'기타' 카테고리의 다른 글
aws CloudFront + S3 (3) | 2023.02.14 |
---|---|
Swagger + Security 적용 (2) | 2023.01.26 |
R data 연습 다루기 (0) | 2022.10.25 |
docker 안에 있는 mariaDB 접속하기 (0) | 2022.09.15 |
docker mailserver 메일이 발송이 안될때 (0) | 2022.09.14 |