🤔 SpringSecurity를 사용하지 않았던 경우!
- JWT Token을 이용해 인가, 인증을 연습하고 있다
- 만약 header의 token이 정상 유저로 판명됐다면,
RequestContextHolder에 저장해두었던, 사용자 정보를 가져와서 로그인한 user의 정보를 확인하였다- 로그인이 필요한 모든 페이지에서 해당 작업을 하기엔 코드에 중복이 너무 많으므로, 어노테이션을 하나 만들어서 ->
HandlerMethodArgumentResolver를 통해SRP원칙을 지켜주었다
@Configuration 과 @EnableWebSecurity 어노테이션을 통해 -> Spring Security의 설정파일임을 알려준다@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.csrf((auth) -> auth.disable()) // csrf disable(Session 방식에서는 필요, JWT 방식에서는 필요 없음)
.formLogin((auth) -> auth.disable()) // from 로그인 방식 disable
.httpBasic((auth) -> auth.disable()) // http basic 인증 방식 disable
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // JWT 방식 -> Session 을 Stateless한 상태로 관리!
return httpSecurity.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
authorizeHttpRequests를 통해 경로별 인가작업을 진행permitAll()requestMatchers()를 통해 경로 작업을 해줄 수 있다anyRequest().authenticated() : 이외의 주소는, 인가작업 후 접근 가능sessionManagement()sessionCreationPolicy()
Spring Security 공식문서
https://spring.io/projects/spring-security#learn
https://docs.spring.io/spring-security/reference/servlet/architecture.html




우리는 formLogin을 disable 해두었기 때문에 커스텀한
UsernamePasswordAuthenticationFilter을 만들어서 등록해주면 된다
attemptAuthentication에서 UsernamePasswordAuthenticationToken에 담아서 AuthenticationManager에 넘겨줘야 한다@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String email = request.getParameter("email");
String password = obtainPassword(request);
// Spring Security 의 Authentication Manager에서 검증하기 위해서는 token에 담아야 함
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null);
// token에 담긴 검증을 위해 Authentication Manager 로 전달
return authenticationManager.authenticate(authToken);
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByEmail(email);
if (userData != null){
return new CustomUserDetails(userData);
}
return null;
}
}
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.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;
}
}
successfulAuthentication 에서 성공 로직을 처리해 주면 된다unsuccessfulAuthentication에서 처리를 해주면 된다@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createJwt(String email, String role, Long expiredMs) {
return Jwts.builder()
.claim("email", email)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
successfulAuthentication 로 이동을 하게 된다Authorization이름, 그리고 Bearer 로 시작하게 넣어준다//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
String email = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String jwt = jwtUtil.createJwt(email, role, 60 * 60 * 10L);
response.addHeader("Authorization","Bearer "+ jwt);
}
//로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
🤔 내 생각 !!!
- JWT Token은 statless하다는 특징이 있지만...
- JWT Token을 사용해 인증할 때에는 Session을 만들어서 사용하자!!
- 실제 우리 대학교 홈페이지도 이렇게 구성되어 있다
- 한번 로그인된 세션에서는 개인정보를 인증해야 하는 페이지에 대해서 계속 이용할 수 있는 것 이다
- 만약 그렇지 않으면, 그 페이지를 인증할 때마다, Token을 클라이언트 사이드에서 서버 사이드로 보내주어야 하고
- 그렇게 되면 Token의 탈취 위험이 증가하게 된다..
OncePerRequestFilter를 구현한 JWTFilter을 사용하자!@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// request에서 Authorization 헤더를 찾음
String authorization = request.getHeader("Authorization");
// Authrization 헤더 검증
if (authorization == null || !authorization.startsWith("Bearer")){
log.info("token null");
filterChain.doFilter(request,response);
// 조건이 해당되면 메서드 종료
return;
}
String token = authorization.split(" ")[1];
// 토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)){
log.info("expired token");
filterChain.doFilter(request,response);
// 조건이 해당되면 메서드 종료
return;
}
//토큰에서 username과 role 획득
String email = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
//userEntity를 생성하여 값 set
UserEntity userEntity = UserEntity.builder()
.email(email)
.password("temppassword")
.role(role)
.build();
//UserDetails에 회원 정보 객체 담기
CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.csrf((auth) -> auth.disable()) // csrf disable(Session 방식에서는 필요, JWT 방식에서는 필요 없음)
.formLogin((auth) -> auth.disable()) // from 로그인 방식 disable
.httpBasic((auth) -> auth.disable()) // http basic 인증 방식 disable
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // JWT 방식 -> Session 을 Stateless한 상태로 관리!
return httpSecurity.build();
}
addFilterAt : 해당 위치에 filter 추가addFilterBefore : 해당 위치 filter 전에 추가addFilterAfter : 해당 위치 filter 후에 추가@RestController
public class AdminController {
@GetMapping("/admin")
public String adminP(){
return "admin Controller";
}
}
로그인을 성공한 후, header로 받은 token값을 이용해서
인가가 필요한 admin 페이지에 해당 token값을 header에 넣은 후 요청을 보내본다

정상 작동하는 모습을 볼 수 있다!!!