목표
- 회원가입 기능
- 로그인 기능
- 로그인 시 JWT 토큰 발급
- JWT 인증, 인가 필터 생성
- JWT의 회원 정보 가져오기
- 인증 객체로 로그인 처리
이전 글 - 회원가입 / 로그인 / JWT 토큰 발급
2022.10.28 - [Spring/Security] - [JWT] 회원가입 / 로그인 / 토큰 발급 (1)
JWT 인증, 인가 필터 생성
인증을 위한 객체, 서비스 생성
PrincipalDetails
- UserDetails를 구현하는 객체
- Entity 클래스인 User에 직접적으로 UserDetails를 구현하는 것은 좋은 방식이 아니다.
- User 객체를 받아 UserDetailsService로 넘겨 주는 역할을한다.
/**
* UserDetails를 이용해서 User객체에 대한 정보를 검색한다.
* UserDetailsService는 인터페이스이므로, 우리가 인증하고자하는 비즈니스로직을 정의한 serivce레이어에서 구현을 실행하는 방식을 이용한다.
*/
@Data
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
user.getRolesList().forEach(r -> {
authorities.add(() -> r);
});
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
PrincipalDetailsService
- UserDetailsService를 구현한다.
- username 기반의 userDetails를 검색한다.
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User userEntity = userRepository.findByEmail(email);
return new PrincipalDetails(userEntity);
}
}
필터 생성
JwtAuthenticationFilter
- 인증(로그인)을 위한 필터
- Spring Security에서 /login 함수 호출 시 이용된다. -> Authentication을 반환한다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtProvider jwtProvider;
// 인증 객체(Authentication)을 만들기 시도
// attemptAuthentication 추상메소드의 구현은 상속한 UsernamePasswordAuthenticationFilter에 구현 되어 있습니다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
ObjectMapper om = new ObjectMapper();
User user = om.readValue(request.getInputStream(), User.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword()
);
/*
- Filter를 통해 AuthenticationToken을 AuthenticationManager에 위임한다.
UsernamePasswordAuthenticationToken 오브젝트가 생성된 후, AuthenticationManager의 인증 메소드를 호출한다.
- PrincipalDetailsService의 loadUserByUsername() 함수가 실행된다.
=> 정상이면 authentication이 반환된다.
* */
Authentication authentication = authenticationManager.authenticate(authenticationToken);
/*
- authentication 객체가 session 영역에 정보를 저장한다. -> 로그인 처리
- authenticatino 객체가 세션에 저장한 '방식'을 반환한다.
- 참고 : security가 권한을 관리해주기 때문에 굳이 JWT에서는 세션을 만들필요는 없지만, 권한 처리를 위해 session을 사용한다.
*/
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println("Authentication 확인: "+principalDetails.getUser().getUsername());
return authentication;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// attemptAuthentication 메소드가 호출 된 후 동작
// response에 JWT 토큰을 담아서 보내준다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
Long id = principalDetails.getUser().getId();
String email = principalDetails.getUser().getEmail();
String userName = principalDetails.getUser().getUsername();
String jwtToken = jwtProvider.generateJwtToken(id, email, userName);
response.addHeader("Authorization", "Bearer " + jwtToken);
}
}
JwtAuthorizationFilter
- 인증이나 권한이 필요한 주소 요청이 있을때 해당 필터를 통과한다.
- Jwt를 검증하고 SpringSecurityContext에 세션을 저장한다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private JwtProvider jwtProvider;
private PrincipalDetailsService principalDetailsService;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider, PrincipalDetailsService principalDetailsService ) {
super(authenticationManager);
this.jwtProvider = jwtProvider;
this.principalDetailsService = principalDetailsService;
}
// 인증이나 권한이 필요한 주소 요청이 있을 때 해당 필터를 통과한다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwtHeader = request.getHeader("Authorization");
// header가 있는지 확인
if (jwtHeader == null || !jwtHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// JWT 토큰을 검증하여 정상적인 사용자인지 확인
String jwtToken = request.getHeader("Authorization").replace("Bearer ","");
User tokenUser = jwtProvider.validToken(jwtToken);
if( tokenUser != null){
PrincipalDetails principalDetails = new PrincipalDetails(tokenUser);
// JWT 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
// 시큐리티 세션에 Authentcation 을 저장한다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
SecurityConfig
- 만들어진 필터들을 등록한다.
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final PrincipalDetailsService principalDetailsService;
private final UserRepository userRepository;
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// ...
// 시큐리티 필터 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable() // http의 기본 인증. ID, PW 인증방식
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtTokenProvider())) // AuthenticationManager
.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtTokenProvider(), principalDetailsService)) // AuthenticationManager
.authorizeHttpRequests()
.anyRequest().permitAll();
return http.build();
}
}
JWT의 회원 정보 가져오기
controller
- JWT의 정보를 가져올 수 있는 방법은 두 가지이다.
- @AuthenticationPrincipal을 이용해 SecurityContext에 있는 세션을 가져오는 방법
- Authentcation 객체를 이용해 AuthenticatioManager, UserDetailsService을 통해 가져오는 방법
@GetMapping("/api/info")
public String info(@AuthenticationPrincipal PrincipalDetails principalDetails, Authentication authentication) {
System.out.println("PrincipalDetails " + principalDetails);
System.out.println("authentication " + authentication);
StringBuilder sb = new StringBuilder();
sb.append("PrincipalDetails ");
sb.append(principalDetails);
sb.append("\n\n");
sb.append("authentication ");
sb.append(authentication);
return sb.toString();
}
- JWT 토큰을 Header 값에 "Authorization"을 key값으로 넣고, value값에 "Bearer " (한 칸 띄어야함. 주의)을 붙여 로그인한 토큰을 붙인다.
PrincipalDetails PrincipalDetails(user=User(id=12, username=username1, email=test1@test.com, password=$2ayi, roles=ROLE_USER, createdDate=2022-10-28T13:02:30.878808))
authentication UsernamePasswordAuthenticationToken [Principal=PrincipalDetails(user=User(id=12, username=username1, email=test1@test.com, password=$2ayi, roles=ROLE_USER, createdDate=2022-10-28T13:02:30.878808)), Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[unit.water.jwt_exam.auth.PrincipalDetails$$Lambda$1201/0x000000080147d278@2d9e1306]]
다음과 같이 @AuthenticationPrincipal 어노테이션을 이용하여 PrincipalDetails 객체를 가져온 값은 PrincipalDetails로 반환해주고, Authentication객체로 가져온 값은 UsernamePasswordAuthenticationToken형태의 인증 객체로 반환해줍니다.
참고 : 만약 Spring Security를 사용하지 않는다면, @AuthenticationPrincipal를 사용할 수 없다. -> ArgurmentResolver나 Interceptor등의 추가 개발을 통해 정보를 가져와야 합니다.
인증 객체로 로그인 처리
service
- PrincipalDetails 객체와 PrincipalDetailsServie 서비스가 있으면 로그인 처리를 다르게 할 수 있습니다.
- 패스워드를 인코딩하여 매칭 시키는 작업을 피할 수 있습니다.
@Service
@RequiredArgsConstructor
public class UserService {
// ...
private final AuthenticationManagerBuilder authenticationManagerBuilder;
// ...
public String login(LoginRequestDto loginRequestDto) {
// String email = loginRequestDto.getEmail();
// String rawPassword = loginRequestDto.getPassword();
//
// User byEmail = userRepository.findByEmail(email);
//
// // 비밀번호 일치 여부 확인
// if(passwordEncoder.matches(rawPassword, byEmail.getPassword())){
//
// // JWT 토큰 반환
// String jwtToken = jwtProvider.generateJwtToken(byEmail.getId(), byEmail.getEmail(), byEmail.getUsername());
//
// return "로그인 성공 " + jwtToken;
// }
// AuthenticationManager에서 처리하는 방법
// AuthentcationManager는 UserDetails로 User 테이블에 접근하여 회원 인증이 가능하다.
String email = loginRequestDto.getEmail();
String password = loginRequestDto.getPassword();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 인증이 완료된 객체이면,
if(authentication.isAuthenticated()) {
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
Long authenticatedId = principalDetails.getUser().getId();
String authenticatedEmail = principalDetails.getUser().getEmail();
String authenticatedUsername = principalDetails.getUser().getUsername();
return "로그인 성공 " + jwtProvider.generateJwtToken(authenticatedId, authenticatedEmail, authenticatedUsername);
}
return "로그인 실패";
}
}
함께보면 도움되는 글
2022.10.21 - [Spring/Security] - [Security] 스프링 시큐리티의 아키텍처(구조) 및 흐름
2022.09.05 - [Spring/Security] - [Security] WebSecurityConfigurerAdapter Deprecated
파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'Spring > Security' 카테고리의 다른 글
[Security] 로그인과 권한 설정 (0) | 2022.11.01 |
---|---|
[Security] 스프링 시큐리티의 아키텍처(구조) 및 흐름 (0) | 2022.11.01 |
[JWT] 회원가입 / 로그인 / 토큰 발급 (1) (0) | 2022.10.28 |
[Security] WebSecurityConfigurerAdapter Deprecated (0) | 2022.09.05 |
Spring Security 기능을 제거하는 간단한 방법 (0) | 2021.09.16 |